diff --git a/Dockerfile b/Dockerfile index 86c17615..e57592a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # WebHarbor — slim, self-contained image. -# 24 Flask mirror sites + control plane on :8101. +# 25 Flask mirror sites + control plane on :8101. FROM python:3.12-slim-bookworm @@ -72,6 +72,15 @@ os.makedirs('instance_seed', exist_ok=True); \ shutil.copy2('instance/rotten_tomatoes.db', 'instance_seed/rotten_tomatoes.db'); \ print('Rotten Tomatoes seed DB generated at build time.')" && rm -rf /opt/WebSyn/rotten_tomatoes/instance -EXPOSE 8101 40000-40023 +# AccuWeather uses genuine captured UI assets and freezes its deterministic seed. +RUN test -n "$(ls -A /opt/WebSyn/accuweather/static/images)" && \ + cd /opt/WebSyn/accuweather && rm -rf instance instance_seed && python3 -c "\ +import app; \ +import os, shutil; \ +os.makedirs('instance_seed', exist_ok=True); \ +shutil.copy2('instance/accuweather.db', 'instance_seed/accuweather.db'); \ +print('AccuWeather seed DB generated at build time.')" && rm -rf /opt/WebSyn/accuweather/instance + +EXPOSE 8101 40000-40024 CMD ["/opt/websyn_start.sh"] diff --git a/control_server.py b/control_server.py index 7df0d9ee..fdcd50a1 100644 --- a/control_server.py +++ b/control_server.py @@ -26,7 +26,7 @@ 'allrecipes', 'amazon', 'apple', 'arxiv', 'bbc_news', 'booking', 'github', 'google_flights', 'google_map', 'google_search', 'huggingface', 'wolfram_alpha', 'cambridge_dictionary', - 'coursera', 'espn', 'merriam_webster', 'ikea', 'phys_org', 'target', 'ted', 'osu', 'rotten_tomatoes', 'compass', 'walmart_careers', + 'coursera', 'espn', 'merriam_webster', 'ikea', 'phys_org', 'target', 'ted', 'osu', 'rotten_tomatoes', 'compass', 'walmart_careers', 'accuweather', ] BASE_PORT = 40000 WEBSYN_DIR = '/opt/WebSyn' diff --git a/review-reports/ACCUWEATHER-FINAL-AUDIT.md b/review-reports/ACCUWEATHER-FINAL-AUDIT.md new file mode 100644 index 00000000..bad597f2 --- /dev/null +++ b/review-reports/ACCUWEATHER-FINAL-AUDIT.md @@ -0,0 +1,54 @@ +# AccuWeather final task and UI audit + +Audit date: 2026-09-09. Branch: `add-accuweather-mirror`. + +The site was reset before every task and browser cookies were cleared. Every run began at `http://localhost:41024/` and used Playwright visible-element locators to search, open results, select tabs, sign in, change preferences, and submit forms. No task used a direct destination URL, database lookup, or source-code answer. + +| Task | Result | Flow checked | Screenshot | +|---|---|---|---| +| AccuWeather--0 | Pass | Search → Phoenix current conditions | `outputs/accuweather-review/0-99-final.png` | +| AccuWeather--1 | Pass | Ambiguous Portland search → Maine result | `outputs/accuweather-review/1-99-final.png` | +| AccuWeather--2 | Pass | Search → Seattle → Hourly | `outputs/accuweather-review/2-99-final.png` | +| AccuWeather--3 | Pass | Search → Miami → Daily | `outputs/accuweather-review/3-99-final.png` | +| AccuWeather--4 | Pass | Search and compare Austin / Denver | `outputs/accuweather-review/4-99-final.png` | +| AccuWeather--5 | Pass | Springfield distractors → Missouri → Air Quality | `outputs/accuweather-review/5-99-final.png` | +| AccuWeather--6 | Pass | Login → search → save Seattle → account | `outputs/accuweather-review/6-99-final.png` | +| AccuWeather--7 | Pass | Login → Boston → remove → account | `outputs/accuweather-review/7-99-final.png` | +| AccuWeather--8 | Pass | Login → Chicago → alert form submit | `outputs/accuweather-review/8-99-final.png` | +| AccuWeather--9 | Pass | Login → Settings → Celsius → New York | `outputs/accuweather-review/9-99-final.png` | +| AccuWeather--10 | Pass | Postal search → San Francisco | `outputs/accuweather-review/10-99-final.png` | +| AccuWeather--11 | Pass | Search → radar → current weather | `outputs/accuweather-review/11-99-final.png` | +| AccuWeather--12 | Pass | Compare two Air Quality pages | `outputs/accuweather-review/12-99-final.png` | +| AccuWeather--13 | Pass | Search → Boston seven-day forecast | `outputs/accuweather-review/13-99-final.png` | +| AccuWeather--14 | Pass | London current → Air Quality | `outputs/accuweather-review/14-99-final.png` | +| AccuWeather--15 | Pass | Login → save two cities → account | `outputs/accuweather-review/15-99-final.png` | +| AccuWeather--16 | Pass | Portland Oregon / Maine comparison | `outputs/accuweather-review/16-99-final.png` | +| AccuWeather--17 | Pass | Search → Toronto hourly forecast | `outputs/accuweather-review/17-99-final.png` | +| AccuWeather--18 | Pass | Register → save Atlanta → account | `outputs/accuweather-review/18-99-final.png` | +| AccuWeather--19 | Pass | Phoenix / New Orleans comparison | `outputs/accuweather-review/19-99-final.png` | + +## Hardening results + +- De-leak: search results disclose only location identity and postal code, never current, hourly, daily, or air-quality answers. The prompt wording, ordering, labels, and cards do not reveal computed answers. +- Distractors: ambiguous Portland and Springfield queries return two and three same-name locations respectively; the 20-location catalog also supplies realistic cross-city comparison choices. +- Catalog breadth: 20 locations, 140 daily rows, and 240 hourly rows cover United States, Canada, and United Kingdom conditions and all captured icon types. +- Cross-field consistency: current temperatures, derived forecasts, unit conversion, conditions, location identity, saved locations, and alerts use relational records shared by all views. +- Leak archetypes checked: direct answer in prompt; target first by artificial ordering; pre-sorted winner; count label; detail on result card; unique target without distractors; answer in URL/slug; hidden data attribute; accessible-name answer; placeholder answer; success without mutation; visit-only verification; unrelated-state acceptance. None were found. +- Five hard multi-step tasks: 4, 5, 12, 13, and 19. Tasks 6–9, 15, and 18 additionally verify persistent state changes. + +## Responsive and asset checks + +Home, search, current, hourly, and radar pages were checked at 1440, 390, and 320 CSS pixels (15 page/viewport combinations). All had zero horizontal overflow and zero broken images. The committed screenshots below are representative; the full evidence set and structured step log are in `outputs/accuweather-review/`. + +- `review-reports/assets/accuweather-homepage-1440.png` +- `review-reports/assets/accuweather-homepage-390.png` +- `review-reports/assets/accuweather-homepage-320.png` + +The Solis font, GPS icon, and weather condition SVGs were captured from the live AccuWeather homepage asset inventory. No placeholder image is used. + +## Reset proof + +After the final task audit, `POST /reset/accuweather` completed ready and both files had MD5 `2701fb49e1edc178024785e6f8601870`: + +- `/opt/WebSyn/accuweather/instance/accuweather.db` +- `/opt/WebSyn/accuweather/instance_seed/accuweather.db` diff --git a/review-reports/assets/accuweather-homepage-1440.png b/review-reports/assets/accuweather-homepage-1440.png new file mode 100644 index 00000000..e28754c8 Binary files /dev/null and b/review-reports/assets/accuweather-homepage-1440.png differ diff --git a/review-reports/assets/accuweather-homepage-320.png b/review-reports/assets/accuweather-homepage-320.png new file mode 100644 index 00000000..f7f8bb50 Binary files /dev/null and b/review-reports/assets/accuweather-homepage-320.png differ diff --git a/review-reports/assets/accuweather-homepage-390.png b/review-reports/assets/accuweather-homepage-390.png new file mode 100644 index 00000000..68caefac Binary files /dev/null and b/review-reports/assets/accuweather-homepage-390.png differ diff --git a/sites/accuweather/_health.py b/sites/accuweather/_health.py new file mode 100644 index 00000000..818a7878 --- /dev/null +++ b/sites/accuweather/_health.py @@ -0,0 +1,3 @@ +"""Per-site health probe (optional, called by control_server).""" +def health(): + return {"ok": True, "site": "accuweather"} diff --git a/sites/accuweather/app.py b/sites/accuweather/app.py new file mode 100644 index 00000000..b92b1a06 --- /dev/null +++ b/sites/accuweather/app.py @@ -0,0 +1,141 @@ +"""Deterministic, interaction-complete AccuWeather mirror.""" +import os, re +from datetime import datetime +from functools import wraps +from flask import Flask, flash, redirect, render_template, request, session, url_for +from flask_sqlalchemy import SQLAlchemy +from werkzeug.security import check_password_hash, generate_password_hash + +BASE_DIR=os.path.dirname(os.path.abspath(__file__)) +app=Flask(__name__,instance_path=os.path.join(BASE_DIR,"instance")) +app.config.update(SECRET_KEY="accuweather-local-benchmark",SQLALCHEMY_DATABASE_URI="sqlite:///accuweather.db",SQLALCHEMY_TRACK_MODIFICATIONS=False) +db=SQLAlchemy(app) + +class User(db.Model): + id=db.Column(db.Integer,primary_key=True); email=db.Column(db.String(120),unique=True,nullable=False); name=db.Column(db.String(80),nullable=False); password_hash=db.Column(db.String(255),nullable=False); unit=db.Column(db.String(1),default="F",nullable=False) +class Location(db.Model): + id=db.Column(db.Integer,primary_key=True); slug=db.Column(db.String(100),unique=True,nullable=False); city=db.Column(db.String(80),nullable=False); region=db.Column(db.String(80),nullable=False); country=db.Column(db.String(80),nullable=False); postal=db.Column(db.String(20),nullable=False); temp=db.Column(db.Integer,nullable=False); realfeel=db.Column(db.Integer,nullable=False); condition=db.Column(db.String(80),nullable=False); icon=db.Column(db.String(2),nullable=False); humidity=db.Column(db.Integer,nullable=False); wind=db.Column(db.Integer,nullable=False); visibility=db.Column(db.Integer,nullable=False); pressure=db.Column(db.Float,nullable=False); uv=db.Column(db.Integer,nullable=False); air_quality=db.Column(db.Integer,nullable=False) +class Forecast(db.Model): + id=db.Column(db.Integer,primary_key=True); location_id=db.Column(db.Integer,db.ForeignKey("location.id"),nullable=False); day_index=db.Column(db.Integer,nullable=False); label=db.Column(db.String(30),nullable=False); high=db.Column(db.Integer,nullable=False); low=db.Column(db.Integer,nullable=False); condition=db.Column(db.String(80),nullable=False); icon=db.Column(db.String(2),nullable=False); precip=db.Column(db.Integer,nullable=False); location=db.relationship(Location,backref="forecasts") +class Hourly(db.Model): + id=db.Column(db.Integer,primary_key=True); location_id=db.Column(db.Integer,db.ForeignKey("location.id"),nullable=False); hour_index=db.Column(db.Integer,nullable=False); label=db.Column(db.String(20),nullable=False); temp=db.Column(db.Integer,nullable=False); condition=db.Column(db.String(80),nullable=False); icon=db.Column(db.String(2),nullable=False); precip=db.Column(db.Integer,nullable=False); location=db.relationship(Location,backref="hourly") +class SavedLocation(db.Model): + id=db.Column(db.Integer,primary_key=True); user_id=db.Column(db.Integer,db.ForeignKey("user.id"),nullable=False); location_id=db.Column(db.Integer,db.ForeignKey("location.id"),nullable=False); __table_args__=(db.UniqueConstraint("user_id","location_id"),) +class Alert(db.Model): + id=db.Column(db.Integer,primary_key=True); user_id=db.Column(db.Integer,db.ForeignKey("user.id"),nullable=False); location_id=db.Column(db.Integer,db.ForeignKey("location.id"),nullable=False); alert_type=db.Column(db.String(30),nullable=False); enabled=db.Column(db.Boolean,default=True,nullable=False); __table_args__=(db.UniqueConstraint("user_id","location_id","alert_type"),) + +USERS=[("alice.j@test.com","Alice Johnson"),("bob.smith@test.com","Bob Smith"),("carol.w@test.com","Carol Williams"),("david.b@test.com","David Brown")] +LOCATIONS=[ +("new-york-ny","New York","New York","United States","10007",79,82,"Partly sunny","03",61,9,10,29.92,5,42),("phoenix-az","Phoenix","Arizona","United States","85001",104,115,"Sunny","01",18,7,12,29.75,10,58),("seattle-wa","Seattle","Washington","United States","98101",66,65,"Cloudy","07",73,6,9,30.08,3,24),("miami-fl","Miami","Florida","United States","33101",88,99,"Mostly cloudy","06",76,12,8,29.88,7,36),("chicago-il","Chicago","Illinois","United States","60601",72,71,"Showers","12",70,14,7,29.86,2,31),("boston-ma","Boston","Massachusetts","United States","02108",75,76,"Mostly sunny","02",55,11,10,30.01,6,28),("austin-tx","Austin","Texas","United States","78701",96,103,"Sunny","01",38,10,11,29.81,9,49),("denver-co","Denver","Colorado","United States","80202",84,82,"Partly sunny","03",27,13,15,30.04,8,45),("portland-or","Portland","Oregon","United States","97205",69,68,"Cloudy","07",67,5,10,30.11,3,22),("portland-me","Portland","Maine","United States","04101",70,69,"Mostly cloudy","06",64,8,10,30.02,4,20),("springfield-il","Springfield","Illinois","United States","62701",76,77,"Partly sunny","03",59,10,10,29.91,5,33),("springfield-ma","Springfield","Massachusetts","United States","01103",73,73,"Showers","12",69,7,8,29.98,3,25),("springfield-mo","Springfield","Missouri","United States","65806",81,84,"Mostly sunny","02",53,9,11,29.89,6,39),("san-francisco-ca","San Francisco","California","United States","94102",65,64,"Cloudy","07",75,12,10,30.05,3,18),("los-angeles-ca","Los Angeles","California","United States","90012",78,79,"Mostly sunny","02",49,6,12,29.96,7,41),("atlanta-ga","Atlanta","Georgia","United States","30303",86,91,"Partly sunny","03",58,8,9,29.94,6,46),("nashville-tn","Nashville","Tennessee","United States","37219",84,88,"Mostly cloudy","06",61,7,10,29.93,5,38),("new-orleans-la","New Orleans","Louisiana","United States","70112",89,100,"Showers","12",75,9,7,29.87,4,44),("london-gb","London","England","United Kingdom","SW1A",63,62,"Cloudy","07",72,10,8,30.10,2,21),("toronto-ca","Toronto","Ontario","Canada","M5H",70,69,"Partly sunny","03",62,11,10,29.99,4,29)] + +def seed_benchmark_users(): + if User.query.first(): return + for email,name in USERS: db.session.add(User(email=email,name=name,password_hash=generate_password_hash("TestPass123!"))) + db.session.commit() +def seed_database(): + if Location.query.first(): return + for row in LOCATIONS: + loc=Location(slug=row[0],city=row[1],region=row[2],country=row[3],postal=row[4],temp=row[5],realfeel=row[6],condition=row[7],icon=row[8],humidity=row[9],wind=row[10],visibility=row[11],pressure=row[12],uv=row[13],air_quality=row[14]); db.session.add(loc); db.session.flush() + icons=[loc.icon,"02","03","06","12","07","01"]; conditions=[loc.condition,"Mostly sunny","Partly sunny","Mostly cloudy","Showers","Cloudy","Sunny"] + for i,label in enumerate(["Today","Thu","Fri","Sat","Sun","Mon","Tue"]): db.session.add(Forecast(location_id=loc.id,day_index=i,label=label,high=loc.temp+(i%3)-2,low=loc.temp-12-(i%4),condition=conditions[i],icon=icons[i],precip=(i*17+loc.id*3)%71)) + for i in range(12): + hour=(12+i)%24; label="Now" if i==0 else datetime.strptime(str(hour),"%H").strftime("%-I %p"); db.session.add(Hourly(location_id=loc.id,hour_index=i,label=label,temp=loc.temp+(3-abs(i-4)),condition=conditions[i%7],icon=icons[i%7],precip=(i*11+loc.id)%66)) + db.session.commit() +def seed_preferences(): + if SavedLocation.query.first() or Alert.query.first(): return + alice=User.query.filter_by(email="alice.j@test.com").first() + for slug in ("new-york-ny","boston-ma"): db.session.add(SavedLocation(user_id=alice.id,location_id=Location.query.filter_by(slug=slug).first().id)) + db.session.commit() +with app.app_context(): + os.makedirs(app.instance_path,exist_ok=True); db.create_all(); seed_benchmark_users(); seed_database(); seed_preferences() + +def current_user(): return db.session.get(User,session.get("user_id")) if session.get("user_id") else None +@app.context_processor +def shared(): return {"current_user":current_user(),"unit":session.get("unit",current_user().unit if current_user() else "F")} +def login_required(fn): + @wraps(fn) + def wrapped(*a,**k): + if not current_user(): flash("Please sign in to continue."); return redirect(url_for("login",next=request.path)) + return fn(*a,**k) + return wrapped +def display_temp(value): return round((value-32)*5/9) if session.get("unit",current_user().unit if current_user() else "F")=="C" else value +app.jinja_env.globals.update(display_temp=display_temp) +def scored_locations(query): + terms=set(re.findall(r"[a-z0-9]+",query.lower())); ranked=[] + for loc in Location.query.all(): + fields=[loc.city,loc.region,loc.country,loc.postal]; score=sum(len(terms&set(re.findall(r"[a-z0-9]+",f.lower())))*w for f,w in zip(fields,[5,3,1,6])) + if score: ranked.append((score,loc.city,loc.region,loc)) + return [x[3] for x in sorted(ranked,key=lambda x:(-x[0],x[1],x[2]))] + +@app.route("/") +def index(): return render_template("index.html",location=Location.query.filter_by(slug="new-york-ny").first(),cities=Location.query.limit(8).all()) +@app.route("/search") +def search(): + q=request.args.get("q","").strip(); return render_template("search.html",query=q,locations=scored_locations(q) if q else []) +@app.route("/weather/") +def weather(slug): + loc=Location.query.filter_by(slug=slug).first_or_404(); saved=bool(current_user() and SavedLocation.query.filter_by(user_id=current_user().id,location_id=loc.id).first()); return render_template("weather.html",location=loc,saved=saved) +@app.route("/hourly/") +def hourly(slug): return render_template("hourly.html",location=Location.query.filter_by(slug=slug).first_or_404()) +@app.route("/daily/") +def daily(slug): return render_template("daily.html",location=Location.query.filter_by(slug=slug).first_or_404()) +@app.route("/radar/") +def radar(slug): return render_template("radar.html",location=Location.query.filter_by(slug=slug).first_or_404()) +@app.route("/air-quality/") +def air_quality(slug): return render_template("air_quality.html",location=Location.query.filter_by(slug=slug).first_or_404()) +@app.route("/save/",methods=["POST"]) +@login_required +def save_location(slug): + loc=Location.query.filter_by(slug=slug).first_or_404(); old=SavedLocation.query.filter_by(user_id=current_user().id,location_id=loc.id).first() + if old: db.session.delete(old); flash(f"Removed {loc.city} from saved locations.") + else: db.session.add(SavedLocation(user_id=current_user().id,location_id=loc.id)); flash(f"Saved {loc.city}.") + db.session.commit(); return redirect(request.referrer or url_for("weather",slug=slug)) +@app.route("/alerts/",methods=["GET","POST"]) +@login_required +def alerts(slug): + loc=Location.query.filter_by(slug=slug).first_or_404() + if request.method=="POST": + selected=set(request.form.getlist("alert_type")); Alert.query.filter_by(user_id=current_user().id,location_id=loc.id).delete() + for kind in sorted(selected): db.session.add(Alert(user_id=current_user().id,location_id=loc.id,alert_type=kind,enabled=True)) + db.session.commit(); flash("Alert preferences updated.") + active={a.alert_type for a in Alert.query.filter_by(user_id=current_user().id,location_id=loc.id).all()}; return render_template("alerts.html",location=loc,active=active) +@app.route("/account") +@login_required +def account(): + saved=db.session.query(Location).join(SavedLocation,SavedLocation.location_id==Location.id).filter(SavedLocation.user_id==current_user().id).all(); return render_template("account.html",saved=saved) +@app.route("/settings",methods=["GET","POST"]) +@login_required +def settings(): + if request.method=="POST": + unit=request.form.get("unit") + if unit in {"F","C"}: current_user().unit=unit; session["unit"]=unit; db.session.commit(); flash("Units updated.") + return render_template("settings.html") +@app.route("/units/",methods=["POST"]) +def units(unit): + if unit in {"F","C"}: session["unit"]=unit + return redirect(request.referrer or url_for("index")) +@app.route("/login",methods=["GET","POST"]) +def login(): + if request.method=="POST": + user=User.query.filter_by(email=request.form.get("email","").lower().strip()).first() + if user and check_password_hash(user.password_hash,request.form.get("password","")): session["user_id"]=user.id; session["unit"]=user.unit; flash("Welcome back."); return redirect(request.args.get("next") or url_for("account")) + flash("Email or password is incorrect.") + return render_template("login.html") +@app.route("/register",methods=["GET","POST"]) +def register(): + if request.method=="POST": + email=request.form.get("email","").lower().strip() + if User.query.filter_by(email=email).first(): flash("An account already exists for that email.") + elif len(request.form.get("password",""))<8: flash("Password must be at least 8 characters.") + else: + user=User(email=email,name=request.form.get("name","Weather fan").strip(),password_hash=generate_password_hash(request.form["password"])); db.session.add(user); db.session.commit(); session["user_id"]=user.id; return redirect(url_for("account")) + return render_template("register.html") +@app.route("/logout",methods=["POST"]) +def logout(): session.clear(); return redirect(url_for("index")) +@app.route("/about") +def about(): return render_template("static_page.html",title="About AccuWeather",copy="We provide local weather forecasts and severe weather information for communities around the world.") +@app.route("/privacy") +def privacy(): return render_template("static_page.html",title="Privacy Statement",copy="Your privacy choices and account preferences are available here.") +@app.route("/_health") +def health(): return {"ok":True,"site":"accuweather"} +if __name__=="__main__": app.run(host="0.0.0.0",port=int(os.environ.get("PORT",5000)),debug=False) diff --git a/sites/accuweather/provenance.json b/sites/accuweather/provenance.json new file mode 100644 index 00000000..925ab906 --- /dev/null +++ b/sites/accuweather/provenance.json @@ -0,0 +1,15 @@ +{ + "captured_at": "2026-09-09", + "source": "https://www.accuweather.com/", + "method": "Browser-visible page asset inventory from the rendered AccuWeather homepage", + "assets": { + "static/fonts/Solis-Regular.woff2": "https://www.accuweather.com/fonts/Solis-Regular.woff2", + "static/images/weather-01.svg": "https://www.accuweather.com/images/weathericons/v2a/01.svg", + "static/images/weather-02.svg": "https://www.accuweather.com/images/weathericons/v2a/02.svg", + "static/images/weather-03.svg": "https://www.accuweather.com/images/weathericons/v2a/03.svg", + "static/images/weather-06.svg": "https://www.accuweather.com/images/weathericons/v2a/06.svg", + "static/images/weather-07.svg": "https://www.accuweather.com/images/weathericons/v2a/07.svg", + "static/images/weather-12.svg": "https://www.accuweather.com/images/weathericons/v2a/12.svg", + "static/images/icon-gps.svg": "https://www.awxcdn.com/adc-assets/images/icons/icon-gps.svg" + } +} diff --git a/sites/accuweather/requirements.txt b/sites/accuweather/requirements.txt new file mode 100644 index 00000000..e3e9a71d --- /dev/null +++ b/sites/accuweather/requirements.txt @@ -0,0 +1 @@ +Flask diff --git a/sites/accuweather/static/css/site.css b/sites/accuweather/static/css/site.css new file mode 100644 index 00000000..be5d4a8f --- /dev/null +++ b/sites/accuweather/static/css/site.css @@ -0,0 +1,7 @@ +@font-face{font-family:Solis;src:url('../fonts/Solis-Regular.woff2')}*{box-sizing:border-box}html,body{margin:0;background:#f1f1f1;color:#292929;font-family:Solis,Arial,sans-serif}a{color:inherit;text-decoration:none}button,.button{border:0;border-radius:2px;background:#f05514;color:#fff;padding:12px 22px;font:inherit;cursor:pointer}input{font:inherit}.corp{height:34px;background:#1d1d1d;color:#bbb;display:flex;justify-content:flex-end;gap:28px;padding:8px calc((100% - 1180px)/2);font-size:12px;white-space:nowrap}.breaking{background:#303030;color:#fff;text-align:center;padding:8px;font-size:13px}header{height:66px;background:#222;color:#fff;display:flex;align-items:center;gap:24px;padding:0 max(24px,calc((100% - 1180px)/2));position:relative;z-index:2}.logo{font-size:28px;white-space:nowrap}.logo b{color:#f25b24}.menu{font-size:22px}.search{display:flex;max-width:470px;flex:1;margin:auto}.search input{min-width:0;flex:1;padding:12px;border:0}.search button{font-size:24px;padding:4px 15px}.premium{color:#ffb294}.flash{background:#fff4d5;border-left:5px solid #f05514;max-width:920px;margin:18px auto 0;padding:14px}main{min-height:70vh}.hero{height:170px;background:linear-gradient(135deg,#3d6685,#acc7d8);display:grid;place-items:center}.hero form{display:flex;width:min(700px,90%)}.hero input{flex:1;min-width:0;padding:18px;border:0;font-size:17px}.layout{max-width:1180px;margin:24px auto;display:grid;grid-template-columns:minmax(0,2fr) minmax(260px,1fr);gap:22px}.narrow{max-width:920px;margin:32px auto;padding:0 18px}.card{background:white;box-shadow:0 2px 5px #0002;padding:22px}.eyebrow{font-size:13px;letter-spacing:.08em;color:#666}.current{display:grid;grid-template-columns:1fr 100px 100px 1fr;align-items:center;gap:20px;border-top:3px solid #f05514}.current h1{font-size:23px;margin:0}.current strong,.reading strong{font-size:62px;font-weight:400}.weather-icon{width:72px;height:72px;object-fit:contain}.city-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}.city{display:grid;grid-template-columns:1fr 55px 60px;align-items:center;gap:8px}.city .weather-icon{width:50px;height:50px}.city span{grid-column:1/-1;color:#666}.tabs{display:flex;overflow-x:auto;background:#fff;border-bottom:1px solid #ddd;margin:20px 0}.tabs a{padding:15px 18px;white-space:nowrap}.tabs a:hover{border-bottom:3px solid #f05514}.weather-main{display:grid;grid-template-columns:1fr 1fr;gap:18px;border-top:3px solid #f05514}.reading{display:flex;align-items:center;justify-content:flex-end}.weather-main dl{grid-column:1/-1;margin:0}.weather-main dl div{display:flex;justify-content:space-between;border-top:1px solid #ddd;padding:10px}.weather-main dd{margin:0;font-weight:bold}.actions,.account-head{display:flex;align-items:center;gap:15px;margin:18px 0}.account-head h1{margin-right:auto}.results{background:#fff;box-shadow:0 2px 5px #0002}.result{display:flex;align-items:center;gap:15px;padding:17px;border-bottom:1px solid #ddd}.result img{width:26px;height:26px}.result span{display:flex;flex-direction:column;flex:1}.result small{color:#666;margin-top:4px}.result i{font-size:28px}.list{padding:0}.list>div{display:grid;grid-template-columns:90px 70px 100px 1fr 150px;align-items:center;gap:12px;padding:15px 20px;border-bottom:1px solid #ddd}.list .weather-icon{width:52px;height:52px}.radar{height:440px;position:relative;overflow:hidden;background:repeating-linear-gradient(0deg,transparent 0 39px,#ffffff30 40px),repeating-linear-gradient(90deg,transparent 0 39px,#ffffff30 40px),linear-gradient(145deg,#406942,#a5bf83 40%,#496b48);color:#fff}.radar:after{content:"";position:absolute;inset:25px;border:1px solid #ffffff66;border-radius:50%}.radar span{position:absolute;left:47%;top:51%;z-index:3;background:#222c;padding:4px}.storm{position:absolute;border-radius:50%;filter:blur(8px);z-index:2}.storm.one{width:200px;height:100px;background:#51d969;left:13%;top:16%;box-shadow:45px 15px 15px #ffdb3b}.storm.two{width:170px;height:80px;background:#33b759;right:10%;bottom:15%;box-shadow:-30px -10px 20px #ff9d22}.legend{text-align:right;padding:10px}.legend i{display:inline-block;width:18px;height:10px;background:#51d969;margin-left:20px}.legend i+*{}.aq{display:flex;align-items:center;gap:25px;border-left:7px solid #65a747}.aq-number{font-size:55px;color:#4d8d31}.meter{height:14px;background:#ddd}.meter i{display:block;height:100%;background:#65a747}.auth{max-width:480px;margin:45px auto}.auth form{display:flex;flex-direction:column;gap:18px}.auth label{display:flex;flex-direction:column;gap:7px}.auth input:not([type=radio]):not([type=checkbox]){padding:12px;border:1px solid #aaa}.auth fieldset{display:flex;flex-direction:column;gap:15px;border:1px solid #ddd}.empty{text-align:center}.prose{margin-top:40px}footer{margin-top:60px;background:#252525;color:#bbb;padding:35px max(24px,calc((100% - 1180px)/2))}footer div{display:flex;gap:25px;color:#fff}.sr{position:absolute;left:-9999px} +@media(max-width:700px){.corp{display:none}.breaking{font-size:11px}.premium{display:none}header{padding:0 12px;gap:10px}.logo{font-size:20px}.search input{width:80px}.layout{display:block;margin:16px}.layout aside{margin-top:18px}.current{grid-template-columns:1fr 60px}.current strong{font-size:48px}.current>div:last-child{grid-column:1/-1}.city-grid{grid-template-columns:1fr}.tabs{margin-left:-18px;margin-right:-18px}.weather-main{grid-template-columns:1fr}.reading{justify-content:flex-start}.list>div{grid-template-columns:55px 50px 70px 1fr}.list>div span:last-child{grid-column:1/-1}.radar{height:330px}.account-head{flex-wrap:wrap}.account-head h1{width:100%}footer div{flex-wrap:wrap}} +@media(max-width:360px){header{gap:6px}.logo{font-size:18px}.menu{display:none}.search button{padding:4px 9px}.current{padding:15px}.city{grid-template-columns:1fr 45px 45px}.narrow{padding:0 10px}.tabs{margin-left:-10px;margin-right:-10px}.tabs a{padding:13px 12px}.list>div{padding:12px 10px;gap:7px}} + +/* Captured AccuWeather proportions: a compact 960px weather canvas on a pale-blue page. */ +html,body{background:#e7edf5}.corp{height:32px;padding:7px 20px;justify-content:flex-start;gap:12px}.corp span+span{border-left:1px solid #777;padding-left:12px}.breaking{position:absolute;left:40%;right:0;top:0;height:32px;background:#f15a18;text-align:left;padding:8px 20px;z-index:4}header{height:62px;padding:0 max(20px,calc((100% - 960px)/2));gap:17px}.logo{font-size:25px}.search{height:34px;max-width:325px;margin-left:auto;margin-right:0}.search input{padding:8px 12px;background:#fff}.search button{font-size:18px;padding:2px 12px}.premium{background:white;color:#f05514;padding:6px 10px;border-radius:18px;font-size:11px}.hero{height:auto;background:transparent;display:block;max-width:632px;margin:22px auto 12px}.hero form{width:100%;height:42px;box-shadow:0 1px 4px #0002}.hero input{padding:12px 15px;font-size:14px}.hero button{padding:8px 18px}.layout{max-width:960px;margin:20px auto;grid-template-columns:632px 304px;gap:24px}.layout>section>h2{font-size:19px}.layout aside{box-shadow:none}.card{box-shadow:none}.current{grid-template-columns:1fr 70px 78px 1fr;padding:18px;border-top:0}.current strong,.reading strong{font-size:52px}.city-grid{display:block}.city{min-height:56px;grid-template-columns:1fr 45px 55px 110px;border-bottom:1px solid #d5d8dc;padding:8px 15px}.city span{grid-column:auto;text-align:right}.narrow{max-width:960px;margin:28px auto;padding:0}.narrow>h1,.narrow>p{max-width:632px}.tabs{border-bottom:1px solid #bbc0c7;background:transparent;margin:0 0 315px}.tabs a{padding:13px 20px;color:#656b72;font-size:14px}.tabs a:first-child{border-bottom:2px solid #f05514}.weather-main{width:632px;max-width:100%;border-top:0;padding:12px}.weather-main>div:first-child{grid-column:1/-1;border-bottom:1px solid #ddd;display:flex;justify-content:space-between}.weather-main>div:first-child .eyebrow{margin:0}.weather-main .reading{justify-content:flex-start}.weather-main dl{display:grid;grid-template-columns:1fr 1fr;gap:0 18px}.weather-main dl div{padding:11px 0}.actions{max-width:632px}.results{max-width:632px}.result{background:#fff}.list{max-width:632px}.aq,.radar{max-width:632px}.homepage-title{font-size:13px;letter-spacing:.08em;color:#666}.news-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:1px;background:#ddd}.news-grid article{background:#f5f6f7;padding:15px;min-height:120px}.news-grid h3{font-size:14px;margin:0 0 28px}.news-grid small{color:#777} +@media(max-width:700px){.breaking{position:static;height:auto}.corp{display:none}header{height:54px}.menu{order:4}.logo{font-size:21px}.search{position:absolute;top:58px;left:6px;right:6px;max-width:none;height:36px}.premium{display:none}.hero{margin:48px 6px 12px}.layout{margin:12px 6px;display:block}.layout aside{margin-top:12px}.current{grid-template-columns:minmax(0,1fr) 42px 52px;gap:6px}.current>div:last-child{grid-column:1/-1}.city{grid-template-columns:minmax(0,1fr) 38px 42px 76px;gap:5px;font-size:12px}.narrow{padding:0 8px;margin-top:48px}.tabs{margin:0 -8px 210px}.tabs a{padding:12px 14px}.weather-main{width:100%}.weather-main dl{grid-template-columns:1fr}.news-grid{grid-template-columns:repeat(2,1fr)}} diff --git a/sites/accuweather/static/fonts/Solis-Regular.woff2 b/sites/accuweather/static/fonts/Solis-Regular.woff2 new file mode 100644 index 00000000..0133d024 Binary files /dev/null and b/sites/accuweather/static/fonts/Solis-Regular.woff2 differ diff --git a/sites/accuweather/static/icons/.gitkeep b/sites/accuweather/static/icons/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/accuweather/static/js/.gitkeep b/sites/accuweather/static/js/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/accuweather/tasks.jsonl b/sites/accuweather/tasks.jsonl new file mode 100644 index 00000000..74f7569c --- /dev/null +++ b/sites/accuweather/tasks.jsonl @@ -0,0 +1,20 @@ +{"web_name":"AccuWeather","id":"AccuWeather--0","ques":"Search for Phoenix, Arizona and report the current temperature, RealFeel temperature, and humidity from its current weather page.","web":"http://localhost:40024/","upstream_url":"https://www.accuweather.com/"} +{"web_name":"AccuWeather","id":"AccuWeather--1","ques":"Search for Portland and use the results to open Portland, Maine. Report its current condition, wind speed, and visibility.","web":"http://localhost:40024/","upstream_url":"https://www.accuweather.com/"} +{"web_name":"AccuWeather","id":"AccuWeather--2","ques":"Find Seattle's hourly forecast. Among the displayed hours, identify the first hour with at least a 40% chance of precipitation and report the hour, temperature, and condition.","web":"http://localhost:40024/","upstream_url":"https://www.accuweather.com/"} +{"web_name":"AccuWeather","id":"AccuWeather--3","ques":"Open Miami's daily forecast and identify the displayed day with the greatest chance of precipitation. Report the day, high and low, and condition.","web":"http://localhost:40024/","upstream_url":"https://www.accuweather.com/"} +{"web_name":"AccuWeather","id":"AccuWeather--4","ques":"Compare the current RealFeel temperatures for Austin and Denver. Report both values and which city feels cooler.","web":"http://localhost:40024/","upstream_url":"https://www.accuweather.com/"} +{"web_name":"AccuWeather","id":"AccuWeather--5","ques":"Search for Springfield and open the Missouri result. Report its postal code from the search results, then its UV index and air-quality value from the relevant weather pages.","web":"http://localhost:40024/","upstream_url":"https://www.accuweather.com/"} +{"web_name":"AccuWeather","id":"AccuWeather--6","ques":"Sign in as alice.j@test.com with password TestPass123!, then save Seattle to My AccuWeather. Confirm Seattle appears in Saved Locations.","web":"http://localhost:40024/","upstream_url":"https://www.accuweather.com/"} +{"web_name":"AccuWeather","id":"AccuWeather--7","ques":"Sign in as alice.j@test.com with password TestPass123! and remove Boston from Saved Locations. Confirm Boston is no longer listed there.","web":"http://localhost:40024/","upstream_url":"https://www.accuweather.com/"} +{"web_name":"AccuWeather","id":"AccuWeather--8","ques":"Sign in as bob.smith@test.com with password TestPass123!, open Chicago weather alerts, and enable Severe weather and Rain starting soon only. Save the preferences.","web":"http://localhost:40024/","upstream_url":"https://www.accuweather.com/"} +{"web_name":"AccuWeather","id":"AccuWeather--9","ques":"Sign in as carol.w@test.com with password TestPass123! and change the temperature unit to Celsius. Return to New York current weather and report the displayed temperature and RealFeel in Celsius.","web":"http://localhost:40024/","upstream_url":"https://www.accuweather.com/"} +{"web_name":"AccuWeather","id":"AccuWeather--10","ques":"Use postal-code search for 94102. Open the matching location and report its current condition, humidity, and pressure.","web":"http://localhost:40024/","upstream_url":"https://www.accuweather.com/"} +{"web_name":"AccuWeather","id":"AccuWeather--11","ques":"Find New Orleans and open its radar page. Confirm the radar is for New Orleans, then report the current condition from the Current Weather tab.","web":"http://localhost:40024/","upstream_url":"https://www.accuweather.com/"} +{"web_name":"AccuWeather","id":"AccuWeather--12","ques":"Compare the air-quality values for Los Angeles and San Francisco. Report both values and identify the city with better air quality (the lower value).","web":"http://localhost:40024/","upstream_url":"https://www.accuweather.com/"} +{"web_name":"AccuWeather","id":"AccuWeather--13","ques":"For Boston's seven displayed daily forecasts, find the day with the lowest overnight low. Report the day, low, high, and condition.","web":"http://localhost:40024/","upstream_url":"https://www.accuweather.com/"} +{"web_name":"AccuWeather","id":"AccuWeather--14","ques":"Search for London, England and report the current temperature, wind, humidity, and air-quality category.","web":"http://localhost:40024/","upstream_url":"https://www.accuweather.com/"} +{"web_name":"AccuWeather","id":"AccuWeather--15","ques":"Sign in as david.b@test.com with password TestPass123!, save Phoenix and Miami, and verify both cities appear in Saved Locations.","web":"http://localhost:40024/","upstream_url":"https://www.accuweather.com/"} +{"web_name":"AccuWeather","id":"AccuWeather--16","ques":"Search for Portland and compare the current temperatures of the Oregon and Maine results. Report both and say which is warmer.","web":"http://localhost:40024/","upstream_url":"https://www.accuweather.com/"} +{"web_name":"AccuWeather","id":"AccuWeather--17","ques":"On Toronto's hourly forecast, find the highest displayed temperature. Report the first hour at that temperature and its precipitation chance.","web":"http://localhost:40024/","upstream_url":"https://www.accuweather.com/"} +{"web_name":"AccuWeather","id":"AccuWeather--18","ques":"Create an account named Jamie Lee using jamie.lee@example.test and password Weather123!, save Atlanta, and confirm it is shown in the new account's Saved Locations.","web":"http://localhost:40024/","upstream_url":"https://www.accuweather.com/"} +{"web_name":"AccuWeather","id":"AccuWeather--19","ques":"Compare Phoenix and New Orleans using their current weather pages. Report each city's temperature and humidity, then identify which has the higher heat index using RealFeel as the comparison.","web":"http://localhost:40024/","upstream_url":"https://www.accuweather.com/"} diff --git a/sites/accuweather/templates/.gitkeep b/sites/accuweather/templates/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/accuweather/templates/account.html b/sites/accuweather/templates/account.html new file mode 100644 index 00000000..ceec1ad5 --- /dev/null +++ b/sites/accuweather/templates/account.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block content %}

Saved Locations

{% for l in saved %}{{l.city}}, {{l.region}}{{l.condition}}{{display_temp(l.temp)}}°{% else %}
You have no saved locations.
{% endfor %}
{% endblock %} diff --git a/sites/accuweather/templates/air_quality.html b/sites/accuweather/templates/air_quality.html new file mode 100644 index 00000000..42ce9ac5 --- /dev/null +++ b/sites/accuweather/templates/air_quality.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% from 'macros.html' import tabs %}{% block content %}

{{location.city}} Air Quality

{{tabs(location)}}
{{location.air_quality}}

{{'Good' if location.air_quality<51 else 'Moderate'}}

The air quality is generally acceptable for most individuals.

Current Pollutants

Fine Particulate Matter (PM2.5)

{% endblock %} diff --git a/sites/accuweather/templates/alerts.html b/sites/accuweather/templates/alerts.html new file mode 100644 index 00000000..a00717b8 --- /dev/null +++ b/sites/accuweather/templates/alerts.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block content %}

{{location.city}} Alerts

Choose the notifications you want to receive.

{% endblock %} diff --git a/sites/accuweather/templates/base.html b/sites/accuweather/templates/base.html new file mode 100644 index 00000000..da4bfb01 --- /dev/null +++ b/sites/accuweather/templates/base.html @@ -0,0 +1,5 @@ +{% block title %}AccuWeather{% endblock %} +
AccuWeather for BusinessAccuWeather APIsPodcast
Breaking Weather: Track the latest tropical developments
+
Premium+{% if current_user %}{{current_user.name}}{% else %}Sign in{% endif %}
+{% with msgs=get_flashed_messages() %}{% if msgs %}
{{msgs[-1]}}
{% endif %}{% endwith %}
{% block content %}{% endblock %}
+
AccuWeatherAboutPrivacy

© 2026 AccuWeather, Inc. All Rights Reserved.

diff --git a/sites/accuweather/templates/daily.html b/sites/accuweather/templates/daily.html new file mode 100644 index 00000000..488ac56b --- /dev/null +++ b/sites/accuweather/templates/daily.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% from 'macros.html' import icon,tabs %}{% block content %}

{{location.city}} Daily Weather

{{tabs(location)}}
{% for d in location.forecasts|sort(attribute='day_index') %}
{{d.label}}{{icon(d.icon,d.condition)}}{{display_temp(d.high)}}° / {{display_temp(d.low)}}°{{d.condition}}{{d.precip}}% precipitation
{% endfor %}
{% endblock %} diff --git a/sites/accuweather/templates/hourly.html b/sites/accuweather/templates/hourly.html new file mode 100644 index 00000000..b9ef9245 --- /dev/null +++ b/sites/accuweather/templates/hourly.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% from 'macros.html' import icon,tabs %}{% block content %}

{{location.city}} Hourly Weather

{{tabs(location)}}
{% for h in location.hourly|sort(attribute='hour_index') %}
{{h.label}}{{icon(h.icon,h.condition)}}{{display_temp(h.temp)}}°{{h.condition}}{{h.precip}}% precipitation
{% endfor %}
{% endblock %} diff --git a/sites/accuweather/templates/index.html b/sites/accuweather/templates/index.html new file mode 100644 index 00000000..dfb1fb18 --- /dev/null +++ b/sites/accuweather/templates/index.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% from 'macros.html' import icon %}{% block content %}

UNITED STATES WEATHER

{{location.city}}, {{location.region}}

September 9, 2026 · 12:24 PM

{{icon(location.icon,location.condition)}}{{display_temp(location.temp)}}°
{{location.condition}}
RealFeel® {{display_temp(location.realfeel)}}°

United States Weather Conditions

Weather News

Watching the latest weather patterns across the country

2 hours ago

Prepare for dramatic temperature changes this week

4 hours ago

Severe weather tracker and forecast updates

6 hours ago

What to know before your weekend plans

8 hours ago
{% endblock %} diff --git a/sites/accuweather/templates/login.html b/sites/accuweather/templates/login.html new file mode 100644 index 00000000..d1c3540f --- /dev/null +++ b/sites/accuweather/templates/login.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block content %}

Sign in

New here? Create account

{% endblock %} diff --git a/sites/accuweather/templates/macros.html b/sites/accuweather/templates/macros.html new file mode 100644 index 00000000..6b1c9171 --- /dev/null +++ b/sites/accuweather/templates/macros.html @@ -0,0 +1,2 @@ +{% macro icon(code,alt) %}{{alt}}{% endmacro %} +{% macro tabs(loc) %}{% endmacro %} diff --git a/sites/accuweather/templates/radar.html b/sites/accuweather/templates/radar.html new file mode 100644 index 00000000..2bcc9ba1 --- /dev/null +++ b/sites/accuweather/templates/radar.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% from 'macros.html' import tabs %}{% block content %}

{{location.city}} Weather Radar

{{tabs(location)}}
Light rain Heavy rain
{% endblock %} diff --git a/sites/accuweather/templates/register.html b/sites/accuweather/templates/register.html new file mode 100644 index 00000000..0413e4b2 --- /dev/null +++ b/sites/accuweather/templates/register.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block content %}

Create account

{% endblock %} diff --git a/sites/accuweather/templates/search.html b/sites/accuweather/templates/search.html new file mode 100644 index 00000000..604bd457 --- /dev/null +++ b/sites/accuweather/templates/search.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block title %}Search | AccuWeather{% endblock %}{% block content %}

Search results for “{{query}}”

{% if locations %}{% else %}

No locations found

Try a city, region, country, or postal code.

{% endif %}
{% endblock %} diff --git a/sites/accuweather/templates/settings.html b/sites/accuweather/templates/settings.html new file mode 100644 index 00000000..c938a82b --- /dev/null +++ b/sites/accuweather/templates/settings.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block content %}

Settings

Temperature unit
{% endblock %} diff --git a/sites/accuweather/templates/static_page.html b/sites/accuweather/templates/static_page.html new file mode 100644 index 00000000..74e08403 --- /dev/null +++ b/sites/accuweather/templates/static_page.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block content %}

{{title}}

{{copy}}

Weather with you wherever you go

Accurate, local information helps people plan their day with confidence.

{% endblock %} diff --git a/sites/accuweather/templates/weather.html b/sites/accuweather/templates/weather.html new file mode 100644 index 00000000..d15de9ee --- /dev/null +++ b/sites/accuweather/templates/weather.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% from 'macros.html' import icon,tabs %}{% block title %}{{location.city}} Weather | AccuWeather{% endblock %}{% block content %}

{{location.city}}, {{location.region}}

{{location.country}} Weather

{{tabs(location)}}

CURRENT WEATHER

12:24 PM
{{icon(location.icon,location.condition)}}{{display_temp(location.temp)}}°

{{location.condition}}

RealFeel® {{display_temp(location.realfeel)}}°

Wind
{{location.wind}} mph
Humidity
{{location.humidity}}%
Visibility
{{location.visibility}} mi
Pressure
{{'%.2f'|format(location.pressure)}} in
Manage alerts
{% endblock %} diff --git a/websyn_start.sh b/websyn_start.sh index 9539b4f9..40f24edc 100644 --- a/websyn_start.sh +++ b/websyn_start.sh @@ -5,7 +5,7 @@ set -e SITES=(allrecipes amazon apple arxiv bbc_news booking github google_flights google_map google_search huggingface wolfram_alpha - cambridge_dictionary coursera espn merriam_webster ikea phys_org target ted osu rotten_tomatoes compass walmart_careers) + cambridge_dictionary coursera espn merriam_webster ikea phys_org target ted osu rotten_tomatoes compass walmart_careers accuweather) BASE_PORT=40000 PID_DIR=/tmp/websyn_pids mkdir -p "$PID_DIR"