"""webgym HTTP server: renders spec.py tasks as real pages for headless Chromium. python finetune/webgym/app.py [port=8811] /t// the task's start page (flight / hotel search form, or shop listing) /t///results?... results after submitting a form (hotel: filters + sort that reload with params) /t/shop/?page=&sort=&brand=&q= listing; /t/shop//p/ product page """ import html, json, os, sys, urllib.parse from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import spec as S WIDGETS = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "widgets.js")).read() CSS = """body{font-family:system-ui,sans-serif;margin:0;background:#fafafa;color:#222}header{display:flex;gap:18px;padding:12px 24px;background:#fff;border-bottom:1px solid #ddd} header a{color:#246;text-decoration:none}main{max-width:1000px;margin:0 auto;padding:20px}.hero{background:linear-gradient(#cde,#eef);border-radius:12px;margin-bottom:16px;display:flex;align-items:end;padding:16px;box-sizing:border-box} form,.panel{background:#fff;border:1px solid #ddd;border-radius:12px;padding:16px;display:flex;flex-wrap:wrap;gap:14px;align-items:flex-start} .f{position:relative;min-width:200px}.list{position:absolute;z-index:20;background:#fff;border:1px solid #bbb;list-style:none;margin:0;padding:4px;min-width:240px} .opt{padding:6px 8px;cursor:pointer}.opt:hover{background:#eef}.dlg{position:absolute;z-index:30;background:#fff;border:1px solid #999;border-radius:8px;padding:12px;box-shadow:0 4px 18px #0003;min-width:260px} .cal .grid{display:grid;grid-template-columns:repeat(7,34px);gap:3px}.cal .hdr{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px} button{cursor:pointer}.seg button[aria-pressed=true]{background:#246;color:#fff}.cookie{position:fixed;bottom:0;left:0;right:0;background:#223;color:#fff;padding:18px 24px;z-index:50;display:flex;gap:16px;align-items:center} .card{background:#fff;border:1px solid #ddd;border-radius:10px;padding:12px;margin:10px 0}.grid3{display:grid;grid-template-columns:repeat(4,1fr);gap:10px} .pager{display:flex;gap:10px;margin:24px 0}footer{padding:30px;color:#777}.side{min-width:180px} .err{color:#b00020;margin:4px 0 0;font-size:14px}.sw{border:1px solid #888;border-radius:16px;padding:4px 12px;background:#fff}.sw[aria-checked=true]{background:#246;color:#fff} .slots,.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:4px}.slots button[aria-pressed=true],.chips button[aria-pressed=true]{background:#246;color:#fff} .slots button:disabled{opacity:.4;text-decoration:line-through}.dsel select{margin-right:6px}textarea{width:100%;box-sizing:border-box} .steps{color:#666;margin:0 0 10px}.chiprow{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0}.chip{border:1px solid #999;border-radius:14px;padding:3px 10px;background:#fff} .facet{margin-bottom:14px}.facet b{display:block;margin-bottom:4px}.card .price{font-weight:600}.card .r{color:#a60}""" def page(title, body, gym=None, cookie=False, decoys=()): nav = "".join(f'{html.escape(d)}' for d in decoys) ck = ('') if cookie else "" return (f"{html.escape(title)}" f"
{html.escape(title.split(' ')[0])}{nav}
" f"
{body}
{ck}" f"") SITES = {"flight": ("SkyFinder", "Find your next flight"), "hotel": ("StayFinder", "Find a place to stay"), "car": ("DriveAway", "Rent a car for your trip")} WIDGET_MAP = "({combo:__gym.combo,choice:__gym.choice,date:__gym.date,count:__gym.count,toggle:__gym.toggle,text:__gym.text,slots:__gym.slots,multi:__gym.multi})" def form_page(sp): hosts = "".join(f'
' for k in sp["order"]) site, tagline = SITES[sp["kind"]] hero = f'

{tagline}

' if sp["hero"] else "" fields = [f for f in sp["fields"]] js = f""" const SP={json.dumps({"fields": fields, "kind": sp["kind"], "seed": sp["seed"]})}; for (const f of SP.fields) {{ const h=document.getElementById('host-'+f.key); {WIDGET_MAP}[f.w](h,f); }} const ret=SP.fields.find(f=>f.only_round); if (ret) setInterval(()=>{{ const h=document.getElementById('host-'+ret.key); const show=__gym.state.trip==='round'; if (h.hidden===show) h.hidden=!show; }},100); for (const f of SP.fields.filter(f=>f.show_if)) setInterval(()=>{{ const h=document.getElementById('host-'+f.key); const show=__gym.state[f.show_if[0]]===f.show_if[1]; if (h.hidden===show) {{ h.hidden=!show; if (!show) {{ __gym.state[f.key]=null; }} }} }},100); document.getElementById('go').addEventListener('click',e=>{{ e.preventDefault(); location.href=location.pathname+'/results?'+new URLSearchParams(__gym.serialize(__gym.state)); }}); """ body = (f"{hero}
{hosts}
" f"

Popular: " + ", ".join(f"{c[1]}" for c in S.CITIES[:5]) + "

" f"") return page(site + " – search", body, gym={"kind": sp["kind"], "seed": sp["seed"]}, cookie=sp["cookie"], decoys=sp["decoys"]) def results_page(sp, q): city = {c[0]: c[1] for c in S.CITIES} k = lambda x: (q.get(x) or "").replace("-ap", "") if sp["kind"] == "car": return car_results(sp, q) if sp["kind"] == "flight": head = f"{city.get(k('origin'), '?')} to {city.get(k('destination'), '?')}" chips = [("One way" if q.get("trip") == "oneway" else "Round trip"), f"{q.get('adults', '1')} adult(s)", (q.get("cabin") or "economy").title(), f"Departs {q.get('date', '?')}"] + ([f"Returns {q.get('ret')}"] if q.get("ret") else []) cards = "".join(f"
{7 + i}:{15 * (i % 4):02d} – {9 + i}:{(15 * i + 40) % 60:02d} · {['Nonstop', '1 stop'][i % 2]} · " f"Select ${120 + 37 * i}
" for i in range(5)) body = f"

{head}

{' · '.join(chips)}

{cards}

Modify search

" return page("SkyFinder – results", body, gym={"kind": "flight", "results": dict(q)}, decoys=sp["decoys"]) # hotel results: filters + sort reload the page with params title = f"Stays in {city.get(k('dest'), '?')}" filt = [f for f in ["Free cancellation", "Breakfast included", "Pool", "Pet friendly"]] on = set((q.get("filter") or "").split("|")) - {""} boxes = "".join(f"
" for i, f in enumerate(filt)) sorts = ["Recommended", "Price: low to high", "Guest rating", "Distance to center"] cur = q.get("sort") or "Recommended" sel = "".join(f"" for s in sorts) cards = "".join(f"
Hotel {['Aurora', 'Belvedere', 'Casa Flora', 'Duomo', 'Elba', 'Fjord'][i]} · " f"{8 + (i % 3) * 0.5}/10 · ${90 + 23 * i}/night
" for i in range(6)) js = """function go(p){const u=new URL(location.href);for(const[k,v]of Object.entries(p)){if(v)u.searchParams.set(k,v);else u.searchParams.delete(k)}location.href=u} function setF(f,on){const u=new URL(location.href);const s=new Set((u.searchParams.get('filter')||'').split('|').filter(Boolean));on?s.add(f):s.delete(f);go({filter:[...s].join('|')})}""" body = (f"

{title}

{q.get('cin', '?')} → {q.get('cout', '?')} · {q.get('guests', '2')} guests

" f"
Filters
{boxes}
{cards}

Modify search

") return page("StayFinder – results", body, gym={"kind": "hotel", "results": dict(q)}, decoys=sp["decoys"]) def sp_url(sp): return f"/t/{sp['kind']}/{sp['seed']}" from app_cats import CATS def shop_page(sp, q): items = sp["items"]; L = sp["labels"] if q.get("q"): items = [i for i in items if q["q"] in i["name"].lower()] if q.get("brand"): items = [i for i in items if i["brand"] == q["brand"]] if q.get("cat"): items = [i for i in items if (i["id"] * 7 + len(q["cat"])) % 3 == 0] s = q.get("sort") or "Featured" if s.startswith("Price: low"): items = sorted(items, key=lambda i: i["price"]) elif s.startswith("Price: high"): items = sorted(items, key=lambda i: -i["price"]) elif s == "Newest": items = sorted(items, key=lambda i: -i["id"]) elif s == "Top rated": items = sorted(items, key=lambda i: (i["id"] * 7) % 13) n = int(q.get("page") or 1); per = sp["per_page"]; pages = max(1, -(-len(items) // per)) shown = items[(n - 1) * per: n * per] base = sp_url(sp) def link(**kw): p = {k: v for k, v in {**{k: q.get(k) for k in ("q", "brand", "sort", "cat")}, **kw}.items() if v} return base + ("?" + urllib.parse.urlencode(p) if p else "") grid = "".join(f"
{html.escape(i['name'])}
${i['price']:.2f}
" for i in shown) sorts = ["Featured", "Price: low to high", "Price: high to low", "Newest", "Top rated"] if sp["sort_impl"] == "select": sort_ui = (f" ") else: sort_ui = (f"") brands = "".join(f"
" for b in S.BRANDS) btn = f"" if sp["search_button"] else "" search = (f"
{btn}
") pager = [] if sp["pager"] in ("numbers", "both"): pager += [f"{p}" for p in range(1, pages + 1)] if sp["pager"] in ("next", "both") and n < pages: pager.append(f"{L['next']}") hero = f"

{sp['category']}

" if sp["hero"] else "" body = (f"{hero}
{search} {sort_ui}
{L['brand']}
{brands}" + ("Categories
" + "".join(f"{c}
" for c in CATS) if sp.get("sidebar_cats") else "") + "
" f"

Showing {len(shown)} of {len(items)} products · page {n} of {pages}

{grid}
" f"
") state = {"page": n, "sort": q.get("sort"), "brand": q.get("brand"), "q": q.get("q"), "cat": q.get("cat")} return page(f"{sp['category']} – ShopRight", body, gym={"kind": "shop", "listing": state}, cookie=sp["cookie"] and not q, decoys=["Deals", "Help", "Sign in", "Cart"]) def product_page(sp, pid): it = sp["items"][pid] body = f"

{html.escape(it['name'])}

${it['price']:.2f}

Back to all products" return page(f"{it['name']} – ShopRight", body, gym={"kind": "shop", "product": pid}, decoys=["Deals", "Help", "Sign in", "Cart"]) # ------------------------------------------------------------------------------------------------ car rental results def car_results(sp, q, opened=None): city = {c[0]: c[1] for c in S.CITIES} k = lambda x: (q.get(x) or "").replace("-ap", "") cars = sp["cars"] types_on = set((q.get("ctype") or "").split("|")) - {""} trans = q.get("trans") or "Any" shown = [c for c in cars if (not types_on or c["type"] in types_on) and (trans == "Any" or c["trans"] == trans)] srt = q.get("sort") or "Recommended" if srt.startswith("Price: low"): shown = sorted(shown, key=lambda c: c["price"]) elif srt.startswith("Price: high"): shown = sorted(shown, key=lambda c: -c["price"]) base = f"{sp_url(sp)}/results?" + urllib.parse.urlencode({kk: v for kk, v in q.items() if kk not in ("ctype", "trans", "sort")}) js = r"""function go(p){const u=new URL(location.href);for(const[k,v]of Object.entries(p)){if(v)u.searchParams.set(k,v);else u.searchParams.delete(k)}location.href=u.pathname.replace(/\/c\/\d+$/,'')+u.search} function setT(f,on){const u=new URL(location.href);const s=new Set((u.searchParams.get('ctype')||'').split('|').filter(Boolean));on?s.add(f):s.delete(f);go({ctype:[...s].join('|')})}""" boxes = "".join(f"
" for t in S.CAR_TYPES) radios = "".join(f"
" for t in ["Any", "Automatic", "Manual"]) sorts = ["Recommended", "Price: low to high", "Price: high to low"] sel = "".join(f"" for x in sorts) qs = urllib.parse.urlencode(q) cards = "".join(f"
{html.escape(c['name'])} · {c['type']} · {c['trans']} · {c['seats']} seats · " f"${c['price']}/day
" for c in shown) or "

No cars match these filters.

" tm = lambda x: (q.get(x) or "1000")[:2] + ":" + (q.get(x) or "1000")[2:] head = f"Cars in {city.get(k('pickup'), '?')}" + (f" → {city.get(k('dropoff'), '?')}" if q.get("diff") == "true" and q.get("dropoff") else "") sub = f"{q.get('pdate', '?')} {tm('ptime')} → {q.get('rdate', '?')} {tm('rtime')} · driver {q.get('age', '?')}" if opened is not None: c = cars[opened] body = (f"

{html.escape(c['name'])}

{c['type']} · {c['trans']} · {c['seats']} seats · ${c['price']}/day

{sub}

" f" Back to results · Modify search") return page("DriveAway – car", body, gym={"kind": "car", "results": dict(q), "car": opened}, decoys=sp["decoys"]) body = (f"

{head}

{sub} · Modify search

" f"
Car type{boxes}
Transmission{radios}
" f"
" f"

{len(shown)} cars

{cards}
") return page("DriveAway – results", body, gym={"kind": "car", "results": dict(q)}, decoys=sp["decoys"]) # ------------------------------------------------------------------------------------------------ multi-stage wizards (restaurant, signup) def stage_page(sp, k, q): st = sp["stages"][k - 1]; n = len(sp["stages"]) fields = [] for f in st["fields"]: f = dict(f) if f["key"] in q: f["value"] = q[f["key"]] fields.append(f) hosts = "".join(f'
' for key in st["order"]) hero = f'

{html.escape(sp["site"])}

' if sp["hero"] and k == 1 else "" nxt = f"{sp_url(sp)}/s/{k + 1}" if k < n else f"{sp_url(sp)}/done" back = f"" if k > 1 else "" js = f""" const SP={json.dumps({"fields": fields, "required": st["required"], "keys": [f["key"] for f in st["fields"]]})}; const Q={json.dumps(q)}; for (const f of SP.fields) {{ const h=document.getElementById('host-'+f.key); {WIDGET_MAP}[f.w](h,f); }} const merged=()=>{{ const p=new URLSearchParams(Object.entries(Q).filter(([k])=>!SP.keys.includes(k))); for (const [k,v] of __gym.serialize(__gym.state)) p.set(k,v); return p; }}; function goBack(){{ location.href={json.dumps(f"{sp_url(sp)}/s/{k - 1}")}+'?'+merged(); }} document.getElementById('go').addEventListener('click',e=>{{ e.preventDefault(); let bad=false; for (const f of SP.fields) {{ const miss=SP.required.includes(f.key)&&__gym.empty(__gym.state[f.key]); bad=bad||miss; __gym.error(document.getElementById('host-'+f.key), miss ? (f.w==='toggle' ? 'You must accept this to continue' : f.label.replace(/[?:]$/,'')+' is required') : null); }} if (bad) {{ const s=document.getElementById('formerr'); s.hidden=false; return; }} location.href={json.dumps(nxt)}+'?'+merged(); }}); """ body = (f"{hero}

Step {k} of {n}

{html.escape(st['heading'])}

" f"
{hosts}
{back}
" f"") return page(f"{sp['site']} – {st['heading']}", body, gym={"kind": sp["kind"], "stage": k, "carried": dict(q)}, cookie=sp["cookie"] and k == 1, decoys=sp["decoys"]) def done_page(sp, q): rows = "".join(f"{html.escape(f['label'])}{html.escape(q.get(f['key'], '—'))}" for f in sp["fields"] if f["key"] in q) body = f"

{html.escape(sp['headings']['done'])}

Reference {sp['seed'] % 99991:05d}

{rows}

Start over

" return page(f"{sp['site']} – {sp['headings']['done']}", body, gym={"kind": sp["kind"], "stage": len(sp["stages"]) + 1, "carried": dict(q), "done": True}, decoys=sp["decoys"]) # ------------------------------------------------------------------------------------------------ faceted listing def filter_state(sp, q): if "f" not in q: st = {"brand": [], "color": [], "size": [], "pmin": None, "pmax": None, "rating": None, "sort": None, "page": 1} st.update({k: list(v) for k, v in sp["pre"].items()}) return st return {"brand": [x for x in (q.get("brand") or "").split("|") if x], "color": [x for x in (q.get("color") or "").split("|") if x], "size": [x for x in (q.get("size") or "").split("|") if x], "pmin": q.get("pmin") or None, "pmax": q.get("pmax") or None, "rating": q.get("rating") or None, "sort": q.get("sort") or None, "page": int(q.get("page") or 1)} def filter_page(sp, q): L = sp["labels"]; st = filter_state(sp, q) items = S.apply_listing(sp["items"], st) per = sp["per_page"]; pages = max(1, -(-len(items) // per)); n = min(st["page"], pages) shown = items[(n - 1) * per: n * per] base = sp_url(sp) def link(**kw): p = {**st, **kw} out = {"f": 1} for kk in S.FACETS: if p[kk]: out[kk] = "|".join(p[kk]) for kk in ("pmin", "pmax", "rating", "sort"): if p.get(kk): out[kk] = p[kk] if p.get("page") and p["page"] != 1: out["page"] = p["page"] return base + "?" + urllib.parse.urlencode(out) def toggle_link(fk, v): vals = [x for x in st[fk] if x != v] if v in st[fk] else st[fk] + [v] return link(**{fk: vals, "page": 1}) facets = "" for fk, pool in (("brand", S.BRANDS), ("color", S.COLORS), ("size", S.SIZES)): n_show = sp["show_n"][fk] expanded = any(v in st[fk] for v in pool[n_show:]) opts = lambda vs: "".join(f"
" for v in vs) more = "" if len(pool) > n_show: more = (f"
{opts(pool[n_show:])}
" f"") facets += f"
{L[fk]}{opts(pool[:n_show])}{more}
" rating = "".join(f"
" for v, t in ((None, "Any rating"), ("3", "3 stars & up"), ("4", "4 stars & up"))) price = (f"
– " f"
") chips = [(f"{fk}-{v}", f"{L[fk]}: {v}", link(**{fk: [x for x in st[fk] if x != v], "page": 1})) for fk in S.FACETS for v in st[fk]] if st["pmin"] or st["pmax"]: chips.append(("price", f"Price: {'$' + st['pmin'] if st['pmin'] else ''}{' – ' if st['pmin'] and st['pmax'] else (' and up' if st['pmin'] else 'up to ')}{'$' + st['pmax'] if st['pmax'] else ''}", link(pmin=None, pmax=None, page=1))) if st["rating"]: chips.append(("rating", f"Rating: {st['rating']}+ stars", link(rating=None, page=1))) chiprow = (f"
{L['chips']}:" + "".join(f"" for cid, t, u in chips) + f"Clear all
") if chips else "" srt = st["sort"] or "Featured" if sp["sort_impl"] == "select": sort_ui = (f" ") else: sort_ui = (f"") grid = "".join(f"
{html.escape(i['name'])}
${i['price']:.2f} · ★ {i['rating']} · {i['size']}
" for i in shown) pager = [] if sp["pager"] in ("numbers", "both"): pager += [f"{p}" for p in range(1, pages + 1)] if sp["pager"] in ("next", "both") and n < pages: pager.append(f"{L['next']}") hero = f"

All products

" if sp["hero"] else "" js = f"""function applyPrice(){{const u=new URL({json.dumps(link(page=1))},location.origin);const a=document.querySelector('[data-g=pmin]').value.trim(),b=document.querySelector('[data-g=pmax]').value.trim(); a?u.searchParams.set('pmin',a):u.searchParams.delete('pmin'); b?u.searchParams.set('pmax',b):u.searchParams.delete('pmax'); location.href=u;}}""" body = (f"{hero}
{len(items)} products {sort_ui}
{chiprow}
" f"
{L['price']}{price}
{L['rating']}{rating}
{facets}
" f"

Showing {len(shown)} of {len(items)} · page {n} of {pages}

{grid}
") return page("Everyware – products", body, gym={"kind": "filter", "listing": {**st, "page": n}}, cookie=sp["cookie"] and "f" not in q, decoys=sp["decoys"]) def filter_product_page(sp, pid): it = sp["items"][pid] body = (f"

{html.escape(it['name'])}

${it['price']:.2f} · ★ {it['rating']} · {it['color']} · size {it['size']}

" f" Back to all products") return page(f"{it['name']} – Everyware", body, gym={"kind": "filter", "product": pid}, decoys=sp["decoys"]) class H(BaseHTTPRequestHandler): def log_message(self, *a): pass def do_GET(self): u = urllib.parse.urlparse(self.path); parts = [p for p in u.path.split("/") if p] q = {k: v[0] for k, v in urllib.parse.parse_qs(u.query).items()} try: if len(parts) >= 3 and parts[0] == "t": kind, seed = parts[1], int(parts[2]); sp = S.make(kind, seed) if kind == "shop": out = product_page(sp, int(parts[4])) if len(parts) >= 5 and parts[3] == "p" else shop_page(sp, q) elif kind == "filter": out = filter_product_page(sp, int(parts[4])) if len(parts) >= 5 and parts[3] == "p" else filter_page(sp, q) elif kind in ("restaurant", "signup"): if len(parts) >= 4 and parts[3] == "done": out = done_page(sp, q) else: out = stage_page(sp, int(parts[4]) if len(parts) >= 5 and parts[3] == "s" else 1, q) elif len(parts) >= 4 and parts[3] == "results": out = car_results(sp, q, int(parts[5])) if kind == "car" and len(parts) >= 6 and parts[4] == "c" else results_page(sp, q) else: out = form_page(sp) code = 200 elif parts == ["spec"]: out = json.dumps(S.make(q["kind"], int(q["seed"]))); code = 200 else: out, code = "not found", 404 except Exception as e: out, code = f"error: {type(e).__name__}: {e}", 500 data = out.encode() self.send_response(code); self.send_header("Content-Type", "application/json" if parts == ["spec"] else "text/html; charset=utf-8") self.send_header("Content-Length", str(len(data))); self.end_headers(); self.wfile.write(data) if __name__ == "__main__": port = int(sys.argv[1]) if len(sys.argv) > 1 else 8811 print(f"webgym on http://127.0.0.1:{port}/t/flight/1", flush=True) ThreadingHTTPServer(("127.0.0.1", port), H).serve_forever()