josephrw commited on
Commit
f443ed2
·
verified ·
1 Parent(s): 1e9bf32

Re-restore Giant GPT Terminal Kernel v3.1.1 — GPT relay (overwrite SaaS deploy)

Browse files
.gitignore CHANGED
@@ -1,4 +1,5 @@
1
  __pycache__/
2
  *.pyc
3
  *.pyo
4
- .DS_Store
 
 
1
  __pycache__/
2
  *.pyc
3
  *.pyo
4
+ *.db
5
+ data/
Dockerfile CHANGED
@@ -2,10 +2,13 @@ FROM python:3.11-slim
2
 
3
  WORKDIR /app
4
 
5
- RUN pip install --no-cache-dir fastapi uvicorn[standard] jinja2
6
 
7
  COPY . /app
8
 
 
 
 
9
  EXPOSE 7860
10
 
11
  CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
2
 
3
  WORKDIR /app
4
 
5
+ RUN pip install --no-cache-dir fastapi uvicorn[standard] pydantic
6
 
7
  COPY . /app
8
 
9
+ # Create data directory for bundled DB
10
+ RUN mkdir -p /data
11
+
12
  EXPOSE 7860
13
 
14
  CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,8 +1,8 @@
1
  ---
2
- title: MasseurBoost
3
- emoji: 💆
4
- colorFrom: blue
5
- colorTo: indigo
6
  sdk: docker
7
  app_port: 7860
8
  pinned: false
 
1
  ---
2
+ title: GPT Relay Endpoint
3
+ emoji: 🚀
4
+ colorFrom: gray
5
+ colorTo: blue
6
  sdk: docker
7
  app_port: 7860
8
  pinned: false
app.py CHANGED
@@ -1,483 +1,488 @@
1
- """MasseurBoost Full SaaS deployed on Hugging Face Spaces.
2
 
3
- Self-contained: embeds real scraped databases, serves market intelligence,
4
- profile pages, orchestrator status (read-only), and control panel UI.
5
  """
6
- import json
7
  import os
 
 
 
 
8
  import sqlite3
 
 
9
  from pathlib import Path
10
  from datetime import datetime, timezone
 
11
 
12
- from fastapi import FastAPI, Request, Query
13
- from fastapi.responses import HTMLResponse, JSONResponse
14
  from fastapi.staticfiles import StaticFiles
15
- from fastapi.templating import Jinja2Templates
16
-
17
- BASE_DIR = Path(__file__).parent
18
- TEMPLATES_DIR = BASE_DIR / "templates"
19
- STATIC_DIR = BASE_DIR / "static"
20
- DATA_DIR = BASE_DIR / "data"
21
-
22
- app = FastAPI(title="MasseurBoost", docs_url="/api/docs")
23
-
24
- templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
25
- if STATIC_DIR.exists():
26
- app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
- MASSEURS_DB = DATA_DIR / "masseurs.db"
29
- UNIFIED_DB = DATA_DIR / "unified.db"
30
- CONTENT_POOL_DB = DATA_DIR / "content_pool.db"
31
 
32
- RENTMASSEUR_BASE = "https://rentmasseur.com"
33
 
34
 
35
- def get_masseurs_db():
36
- conn = sqlite3.connect(str(MASSEURS_DB))
37
  conn.row_factory = sqlite3.Row
38
  return conn
39
 
40
 
41
- def get_unified_db():
42
- conn = sqlite3.connect(str(UNIFIED_DB))
43
- conn.row_factory = sqlite3.Row
44
- return conn
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
 
47
- def get_content_pool_db():
48
- conn = sqlite3.connect(str(CONTENT_POOL_DB))
49
- conn.row_factory = sqlite3.Row
50
- return conn
 
 
 
 
 
 
 
 
51
 
52
 
53
- # ─── Landing page ───────────────────────────────────────────
54
- @app.get("/", response_class=HTMLResponse)
55
- async def landing(request: Request):
56
- return templates.TemplateResponse(request, "landing.html")
 
 
 
 
 
57
 
58
 
59
- # ─── Dashboard ──────────────────────────────────────────────
60
- @app.get("/dashboard", response_class=HTMLResponse)
61
- async def dashboard(request: Request):
62
- return templates.TemplateResponse(request, "dashboard.html")
 
63
 
64
 
65
- # ─── Control Panel ──────────────────────────────────────────
66
- @app.get("/control", response_class=HTMLResponse)
67
- async def control_panel(request: Request):
68
- return templates.TemplateResponse(request, "control.html")
69
 
70
 
71
- # ─── Profile page ───────────────────────────────────────────
72
- @app.get("/profile/{username}", response_class=HTMLResponse)
73
- async def profile_page(request: Request, username: str):
74
- conn = get_masseurs_db()
75
- c = conn.cursor()
76
- row = c.execute("SELECT * FROM masseurs WHERE username = ?", (username,)).fetchone()
77
- if not row:
78
- conn.close()
79
- return templates.TemplateResponse(request, "profile.html", {"error": True, "username": username})
80
- profile = dict(row)
81
- total = c.execute("SELECT COUNT(*) FROM masseurs").fetchone()[0]
82
- rank = c.execute("SELECT COUNT(*) FROM masseurs WHERE views_per_day > ?", (profile["views_per_day"],)).fetchone()[0] + 1
83
- percentile = round((1 - rank / total) * 100, 1)
84
- city_peers = c.execute(
85
- "SELECT username, views_per_day, visits FROM masseurs WHERE city = ? AND username != ? ORDER BY views_per_day DESC LIMIT 5",
86
- (profile["city"], username),
87
- ).fetchall()
88
- conn.close()
89
- return templates.TemplateResponse(request, "profile.html", {
90
- "error": False,
91
- "profile": profile,
92
- "rank": rank,
93
- "total": total,
94
- "percentile": percentile,
95
- "city_peers": [dict(r) for r in city_peers],
96
- "services": json.loads(profile.get("services", "[]")),
97
- "rentmasseur_url": f"{RENTMASSEUR_BASE}/{username}",
98
- })
99
-
100
-
101
- # ─── API: Market stats ──────────────────────────────────────
102
- @app.get("/api/market-stats")
103
- async def market_stats():
104
- conn = get_masseurs_db()
105
- c = conn.cursor()
106
- total = c.execute("SELECT COUNT(*) FROM masseurs").fetchone()[0]
107
- cities = c.execute("SELECT COUNT(DISTINCT city) FROM masseurs").fetchone()[0]
108
- gold = c.execute("SELECT COUNT(*) FROM masseurs WHERE is_gold=1").fetchone()[0]
109
- avg_vpd = c.execute("SELECT AVG(views_per_day) FROM masseurs WHERE views_per_day > 0").fetchone()[0]
110
- median_vpd = c.execute("SELECT views_per_day FROM masseurs WHERE views_per_day > 0 ORDER BY views_per_day").fetchall()
111
- median = median_vpd[len(median_vpd)//2][0] if median_vpd else 0
112
 
113
- top_cities = c.execute(
114
- "SELECT city, COUNT(*) as cnt, AVG(views_per_day) as avg_vpd FROM masseurs GROUP BY city ORDER BY cnt DESC LIMIT 10"
115
- ).fetchall()
116
 
117
- top_masseurs = c.execute(
118
- "SELECT username, city, views_per_day, visits, rating, reviews_count, is_gold, photo_count FROM masseurs ORDER BY views_per_day DESC LIMIT 10"
119
- ).fetchall()
 
 
120
 
121
- conn.close()
122
- return {
123
- "total_masseurs": total,
124
- "total_cities": cities,
125
- "gold_members": gold,
126
- "avg_views_per_day": round(avg_vpd or 0, 1),
127
- "median_views_per_day": median,
128
- "top_cities": [{"city": r[0], "count": r[1], "avg_vpd": round(r[2] or 0, 1)} for r in top_cities],
129
- "top_masseurs": [
130
- {"username": r[0], "city": r[1], "vpd": r[2], "visits": r[3], "rating": r[4],
131
- "reviews": r[5], "gold": bool(r[6]), "photos": r[7],
132
- "profile_url": f"{RENTMASSEUR_BASE}/{r[0]}",
133
- "internal_url": f"/profile/{r[0]}"}
134
- for r in top_masseurs
135
- ],
136
- }
137
 
 
 
 
 
138
 
139
- # ─── API: Search masseurs ───────────────────────────────────
140
- @app.get("/api/search")
141
- async def search_masseurs(
142
- city: str = Query("", description="Filter by city"),
143
- q: str = Query("", description="Search by username or city"),
144
- limit: int = Query(20, le=100),
145
- offset: int = Query(0),
146
- sort: str = Query("views_per_day", description="Sort field"),
147
- ):
148
- conn = get_masseurs_db()
149
- c = conn.cursor()
150
- where = "WHERE 1=1"
151
- params = []
152
- if city:
153
- where += " AND city = ?"
154
- params.append(city)
155
- if q:
156
- where += " AND (username LIKE ? OR city LIKE ?)"
157
- params += [f"%{q}%", f"%{q}%"]
158
-
159
- valid_sorts = {"views_per_day", "visits", "reviews_count", "rating", "bio"}
160
- sort = sort if sort in valid_sorts else "views_per_day"
161
-
162
- total = c.execute(f"SELECT COUNT(*) FROM masseurs {where}", params).fetchone()[0]
163
- rows = c.execute(
164
- f"SELECT username, city, views_per_day, visits, since_date, rating, reviews_count, is_gold, photo_count FROM masseurs {where} ORDER BY {sort} DESC LIMIT ? OFFSET ?",
165
- params + [limit, offset],
166
- ).fetchall()
167
 
168
- conn.close()
169
- return {
170
- "total": total,
171
- "results": [
172
- {"username": r[0], "city": r[1], "vpd": r[2], "visits": r[3], "since": r[4],
173
- "rating": r[5], "reviews": r[6], "gold": bool(r[7]), "photos": r[8],
174
- "profile_url": f"{RENTMASSEUR_BASE}/{r[0]}",
175
- "internal_url": f"/profile/{r[0]}"}
176
- for r in rows
177
- ],
178
- }
179
 
180
 
181
- # ─── API: Cities list ───────────────────────────────────────
182
- @app.get("/api/cities")
183
- async def cities():
184
- conn = get_masseurs_db()
185
- c = conn.cursor()
186
- rows = c.execute(
187
- "SELECT city, COUNT(*) as cnt FROM masseurs GROUP BY city ORDER BY cnt DESC"
188
- ).fetchall()
189
- conn.close()
190
- return {"cities": [{"name": r[0], "count": r[1]} for r in rows]}
191
 
192
 
193
- # ─── API: Orchestrator status (read-only on HF) ─────────────
194
- @app.get("/api/orchestrator/status")
195
- async def orch_status():
196
- return {
197
- "daemon_running": False,
198
- "daemon_note": "Running on Hugging Face Spaces — daemon operates from local Mac. Visit local control panel for live daemon control.",
199
- "tasks": [],
200
- "log_tail": [],
201
- }
202
 
203
 
204
- # ─── API: DB stats ──────────────────────────────────────────
205
- @app.get("/api/orchestrator/db-stats")
206
- async def db_stats():
207
- stats = {}
208
- if UNIFIED_DB.exists():
209
- u = get_unified_db()
210
- stats["visitors"] = u.execute("SELECT COUNT(*) FROM visitors").fetchone()[0]
211
- stats["visit_log"] = u.execute("SELECT COUNT(*) FROM visit_log").fetchone()[0]
212
- stats["hourly_kpis"] = u.execute("SELECT COUNT(*) FROM hourly_kpis").fetchone()[0]
213
- stats["profile_stats"] = u.execute("SELECT COUNT(*) FROM my_profile_stats").fetchone()[0]
214
- try:
215
- stats["bio_experiments"] = u.execute("SELECT COUNT(*) FROM bio_experiments").fetchone()[0]
216
- except Exception:
217
- stats["bio_experiments"] = 0
218
- try:
219
- stats["blog_posts"] = u.execute("SELECT COUNT(*) FROM blog_posts").fetchone()[0]
220
- except Exception:
221
- stats["blog_posts"] = 0
222
- try:
223
- stats["interview_sets"] = u.execute("SELECT COUNT(*) FROM interview_sets").fetchone()[0]
224
- except Exception:
225
- stats["interview_sets"] = 0
226
- try:
227
- stats["receipts"] = u.execute("SELECT COUNT(*) FROM my_receipts").fetchone()[0]
228
- except Exception:
229
- stats["receipts"] = 0
230
- row = u.execute("SELECT * FROM hourly_kpis ORDER BY id DESC LIMIT 1").fetchone()
231
- if row:
232
- cols = [d[0] for d in u.execute("SELECT * FROM hourly_kpis LIMIT 0").description]
233
- stats["latest_kpi"] = dict(zip(cols, row))
234
- u.close()
235
- if MASSEURS_DB.exists():
236
- m = get_masseurs_db()
237
- stats["masseurs_scraped"] = m.execute("SELECT COUNT(*) FROM masseurs").fetchone()[0]
238
- stats["cities_scraped"] = m.execute("SELECT COUNT(DISTINCT city) FROM masseurs").fetchone()[0]
239
- m.close()
240
- if CONTENT_POOL_DB.exists():
241
- cp = get_content_pool_db()
242
- stats["content_variants"] = cp.execute("SELECT COUNT(*) FROM content_pool").fetchone()[0]
243
- stats["competitor_benchmarks"] = cp.execute("SELECT COUNT(*) FROM competitor_benchmarks").fetchone()[0]
244
- cp.close()
245
- return stats
246
-
247
-
248
- # ─── API: Orchestrator logs (read from embedded state) ──────
249
- @app.get("/api/orchestrator/logs")
250
- async def orch_logs(limit: int = 100):
251
- return {"logs": [], "note": "Logs available on local control panel"}
252
-
253
-
254
- # ─── API: Trigger tasks (disabled on HF) ────────────────────
255
- @app.post("/api/orchestrator/trigger/{task_name}")
256
- async def orch_trigger(task_name: str):
257
- return JSONResponse(
258
- {"error": "Task triggering is only available on the local control panel (localhost:8000)"},
259
- status_code=503,
260
- )
261
-
262
-
263
- @app.post("/api/orchestrator/daemon/start")
264
- async def daemon_start():
265
- return JSONResponse(
266
- {"error": "Daemon control is only available on the local control panel (localhost:8000)"},
267
- status_code=503,
268
- )
269
-
270
-
271
- @app.post("/api/orchestrator/daemon/stop")
272
- async def daemon_stop():
273
- return JSONResponse(
274
- {"error": "Daemon control is only available on the local control panel (localhost:8000)"},
275
- status_code=503,
276
- )
277
-
278
-
279
- # ─── API: Competitors by city ───────────────────────────────
280
- @app.get("/api/competitors/{city}")
281
- async def competitors(city: str, limit: int = Query(20, le=100)):
282
- conn = get_masseurs_db()
283
- c = conn.cursor()
284
- rows = c.execute(
285
- "SELECT username, city, views_per_day, visits, rating, reviews_count, is_gold, photo_count FROM masseurs WHERE city = ? ORDER BY views_per_day DESC LIMIT ?",
286
- (city, limit),
287
- ).fetchall()
288
- conn.close()
289
- return {
290
- "city": city,
291
- "count": len(rows),
292
- "competitors": [
293
- {"username": r[0], "city": r[1], "vpd": r[2], "visits": r[3],
294
- "rating": r[4], "reviews": r[5], "gold": bool(r[6]), "photos": r[7],
295
- "profile_url": f"{RENTMASSEUR_BASE}/{r[0]}"}
296
- for r in rows
297
- ],
298
- }
299
 
300
 
301
- # ─── API: Optimization status ───────────────────────────────
302
- @app.get("/api/optimization-status")
303
- async def optimization_status():
304
- return {
305
- "engine": "Genetic Algorithm + Reinforcement Learning + LLM",
306
- "components": [
307
- {"name": "Bio Optimizer", "status": "active", "description": "GA evolves bio text, RL tracks engagement, LLM generates mutations"},
308
- {"name": "Blog Generator", "status": "active", "description": "LLM generates SEO-optimized blog posts from winning patterns"},
309
- {"name": "Photo Rotator", "status": "active", "description": "GA tests photo order combinations for max click-through"},
310
- {"name": "Availability Engine", "status": "active", "description": "RL optimizes availability windows for booking conversion"},
311
- {"name": "Interview Rotator", "status": "active", "description": "LLM generates funny Q&A that drive engagement"},
312
- {"name": "Price Optimizer", "status": "active", "description": "GA tests price points for revenue maximization"},
313
- ],
314
- "cycle": "Every 3 hours: collect stats → evaluate fitness → mutate → push winners → repeat",
315
- }
316
 
317
 
318
- # ─── API: Blog topics ───────────────────────────────────────
319
- @app.get("/api/blog/topics")
320
- async def blog_topics():
321
- topics = [
322
- {"id": "perfect_session", "title": "The Perfect Session", "angle": "What makes a 5-star massage experience"},
323
- {"id": "client_etiquette", "title": "Client Etiquette 101", "angle": "How to be the client therapists love"},
324
- {"id": "deep_tissue_magic", "title": "Deep Tissue Magic", "angle": "Why deep tissue isn't scary"},
325
- {"id": "midnight_massage", "title": "Midnight Massage Stories", "angle": "The weird and wonderful night crowd"},
326
- {"id": "luxury_experience", "title": "Luxury on a Budget", "angle": "Premium feel without the spa price"},
327
- {"id": "recovery_secrets", "title": "Recovery Secrets", "angle": "Post-workout massage for athletes"},
328
- {"id": "first_time_guide", "title": "First Time? Here's What to Expect", "angle": "Beginner-friendly guide"},
329
- {"id": "stress_relief", "title": "Stress Relief That Actually Works", "angle": "Why massage beats meditation"},
330
- {"id": "weekend_reset", "title": "The Weekend Reset", "angle": "Friday massage = Monday productivity"},
331
- {"id": "funniest_requests", "title": "Funniest Client Requests", "angle": "Humor drives engagement and bookings"},
332
- ]
333
- return {"topics": topics}
334
-
335
-
336
- # ─── API: Photo strategy ────────────────────────────────────
337
- @app.get("/api/photo-strategy")
338
- async def photo_strategy():
339
- return {
340
- "strategy": "organism",
341
- "description": "Your profile photos rotate automatically based on time of day, day of week, and engagement patterns.",
342
- "schedule": [
343
- {"time": "Morning (6am-12pm)", "type": "Professional/clinical shots", "reason": "Business professionals browsing before work"},
344
- {"time": "Afternoon (12pm-6pm)", "type": "Action/massage technique shots", "reason": "Lunch break browsers want to see skills"},
345
- {"time": "Evening (6pm-12am)", "type": "Lifestyle/relaxation shots", "reason": "After-work crowd wants to envision the experience"},
346
- {"time": "Late night (12am-6am)", "type": "Mood/atmosphere shots", "reason": "Night owls respond to ambiance over clinical"},
347
- ],
348
- "rotation_frequency": "Every 6 hours",
349
- "ai_optimization": "GA optimizer tests photo order combinations and picks the one with highest click-through rate",
350
- }
351
 
352
 
353
- # ─── API: Availability gravity ──────────────────────────────
354
- @app.get("/api/availability-gravity")
355
- async def availability_gravity():
356
- return {
357
- "feature": "Availability Gravity",
358
- "description": "Dynamic availability updates that create urgency and drive repeat visits.",
359
- "tactics": [
360
- {"name": "Scarcity Windows", "description": "Show limited time slots to create urgency", "impact": "Increases booking conversion by 40-60%"},
361
- {"name": "Dynamic Status", "description": "Auto-update status to 'Available Now' during peak browsing hours", "impact": "Captures impulse bookings"},
362
- {"name": "Revisit Triggers", "description": "Rotate availability display so returning visitors see new slots", "impact": "Increases daily return visits by 25%"},
363
- {"name": "Peak Hour Alignment", "description": "Open availability slots during historically high-traffic periods", "impact": "Maximizes visibility when most clients are browsing"},
364
- ],
365
- "rl_optimization": "Reinforcement learning loop tracks which availability patterns drive the most bookings and auto-adjusts.",
366
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
 
368
 
369
- # ─── API: Pricing ───────────────────────────────────────────
370
- @app.get("/api/pricing")
371
- async def pricing():
372
- return {
373
- "plans": [
374
- {"name": "Starter", "price": "$49/mo", "features": ["AI-optimized bio (monthly refresh)", "2 blog posts per month", "Basic photo rotation", "Market intelligence report", "Email support"]},
375
- {"name": "Professional", "price": "$149/mo", "popular": True, "features": ["GA/RL bio optimization (weekly A/B testing)", "4 blog posts per month", "Smart photo rotation", "Availability gravity engine", "Interview Q&A rotation", "Competitor benchmarking", "Priority support"]},
376
- {"name": "Elite", "price": "$399/mo", "features": ["Full autonomous optimization", "Unlimited blog posts + interviews", "Advanced photo rotation (GA-optimized)", "Full availability gravity + price optimization", "Worldwide competitor intelligence", "Custom LLM fine-tuning", "Dedicated account manager", "API access"]},
377
- ]
378
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
 
380
 
381
- # ─── API: Bio preview ───────────────────────────────────────
382
- @app.get("/api/bio-preview")
383
- async def bio_preview(style: str = Query("balanced")):
384
- previews = {
385
- "balanced": "Certified massage therapist with years of experience in deep tissue, Swedish, and therapeutic techniques. Discreet, professional, and dedicated to your relaxation. Available now in Manhattan — call to book your session today.",
386
- "funny": "I'm not saying my hands are magic, but clients have been known to forget their own name mid-massage. Certified, discreet, and surprisingly funny for someone who works in silence. Available now — your muscles are begging you to call.",
387
- "luxury": "An exclusive massage experience for the discerning gentleman. Private studio, premium oils, and techniques refined over thousands of sessions. When only the best will do. By appointment only — call now to secure your time slot.",
388
- "clinical": "Licensed massage therapist specializing in deep tissue recovery, sports massage, and chronic pain management. Evidence-based techniques, professional environment, proven results. Direct booking available — call to schedule your consultation.",
389
- "mysterious": "Some experiences can't be described — only felt. Step into a world of sensory exploration where every touch tells a story. Discreet, exclusive, unforgettable. The key is in your hands — call to unlock.",
390
- }
391
- return {"style": style, "preview": previews.get(style, previews["balanced"])}
 
 
 
 
 
 
 
 
 
 
392
 
393
 
394
- # ─── API: Health ────────────────────────────────────────────
395
- @app.get("/api/health")
396
- async def health():
397
- return {
398
- "status": "ok",
399
- "masseurs_db": str(MASSEURS_DB),
400
- "masseurs_db_exists": MASSEURS_DB.exists(),
401
- "unified_db_exists": UNIFIED_DB.exists(),
402
- "content_pool_db_exists": CONTENT_POOL_DB.exists(),
403
- }
404
 
405
 
406
- # ─── API: Visitors data ─────────────────────────────────────
407
- @app.get("/api/visitors")
408
- async def visitors(limit: int = Query(50, le=200)):
409
- conn = get_unified_db()
410
- c = conn.cursor()
411
- rows = c.execute(
412
- "SELECT username, visit_count, last_online, profile_url, profile_hash FROM visitors ORDER BY visit_count DESC LIMIT ?",
413
- (limit,),
414
- ).fetchall()
415
- conn.close()
416
- return {
417
- "count": len(rows),
418
- "visitors": [
419
- {"username": r[0], "visit_count": r[1], "last_online": r[2],
420
- "profile_url": r[3], "profile_hash": r[4]}
421
- for r in rows
422
- ],
423
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
 
425
 
426
- # ─── API: KPI history ───────────────────────────────────────
427
- @app.get("/api/kpis")
428
- async def kpi_history(limit: int = Query(50, le=200)):
429
- conn = get_unified_db()
430
- c = conn.cursor()
431
- rows = c.execute(
432
- "SELECT id, timestamp, immortality_score, immortality_grade, virality_score, virality_grade, profile_views, contact_clicks, new_visits, views_per_day FROM hourly_kpis ORDER BY id DESC LIMIT ?",
433
- (limit,),
434
- ).fetchall()
435
- conn.close()
436
- return {
437
- "count": len(rows),
438
- "kpis": [
439
- {"id": r[0], "timestamp": r[1], "immortality_score": r[2], "immortality_grade": r[3],
440
- "virality_score": r[4], "virality_grade": r[5], "profile_views": r[6],
441
- "contact_clicks": r[7], "new_visits": r[8], "views_per_day": r[9]}
442
- for r in rows
443
- ],
444
- }
445
 
446
 
447
- # ─── API: Content pool ──────────────────────────────────────
448
- @app.get("/api/content-pool")
449
- async def content_pool(limit: int = Query(50, le=200)):
450
- conn = get_content_pool_db()
451
- c = conn.cursor()
452
- rows = c.execute(
453
- "SELECT * FROM content_pool ORDER BY benchmark_rank ASC LIMIT ?",
454
- (limit,),
455
- ).fetchall()
456
- conn.close()
457
- cols = [d[0] for d in c.description] if c.description else []
458
- return {
459
- "count": len(rows),
460
- "content": [dict(zip(cols, r)) for r in rows],
461
- }
462
 
463
 
464
- # ─── API: Competitor benchmarks ─────────────────────────────
465
- @app.get("/api/competitor-benchmarks")
466
- async def competitor_benchmarks(limit: int = Query(50, le=200)):
467
- conn = get_content_pool_db()
468
- c = conn.cursor()
469
- rows = c.execute(
470
- "SELECT * FROM competitor_benchmarks ORDER BY id DESC LIMIT ?",
471
- (limit,),
472
- ).fetchall()
473
- conn.close()
474
- cols = [d[0] for d in c.description] if c.description else []
475
  return {
476
- "count": len(rows),
477
- "benchmarks": [dict(zip(cols, r)) for r in rows],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
  }
479
-
480
-
481
- if __name__ == "__main__":
482
- import uvicorn
483
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
+ """Giant GPT Terminal Kernel josephrw-endpoint HF Space.
2
 
3
+ GPT Actions compatible external terminal, Python, file, artifact URL,
4
+ dynamic tool, memory, and receipt kernel. Hardened to avoid post-execution 500s.
5
  """
 
6
  import os
7
+ import json
8
+ import time
9
+ import uuid
10
+ import hashlib
11
  import sqlite3
12
+ import subprocess
13
+ import threading
14
  from pathlib import Path
15
  from datetime import datetime, timezone
16
+ from typing import Optional
17
 
18
+ from fastapi import FastAPI, Request, Query, HTTPException
19
+ from fastapi.responses import JSONResponse, PlainTextResponse
20
  from fastapi.staticfiles import StaticFiles
21
+ from pydantic import BaseModel, Field
22
+
23
+ app = FastAPI(
24
+ title="Giant GPT Terminal Kernel",
25
+ version="3.1.1",
26
+ docs_url="/docs",
27
+ openapi_url="/openapi.json",
28
+ )
29
+
30
+ # ─── Paths ────────────────────────────────────────────────────
31
+ WORKSPACE_ROOT = Path(os.environ.get("WORKSPACE_ROOT", str(Path(__file__).parent / "data" / "workspaces")))
32
+ ARTIFACTS_DIR = Path(os.environ.get("ARTIFACTS_DIR", str(Path(__file__).parent / "data" / "artifacts")))
33
+ DB_PATH = Path(os.environ.get("KERNEL_DB", str(Path(__file__).parent / "data" / "kernel.db")))
34
+
35
+ WORKSPACE_ROOT.mkdir(parents=True, exist_ok=True)
36
+ ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True)
37
+
38
+ # ─── SQLite for receipts, memory, tools, sessions ─────────────
39
+ def _init_db():
40
+ conn = sqlite3.connect(str(DB_PATH))
41
+ c = conn.cursor()
42
+ c.execute("""CREATE TABLE IF NOT EXISTS receipts (
43
+ id TEXT PRIMARY KEY, kind TEXT, workspace TEXT, summary TEXT,
44
+ sha256 TEXT, created_at TEXT, data_json TEXT
45
+ )""")
46
+ c.execute("""CREATE TABLE IF NOT EXISTS memory (
47
+ id TEXT PRIMARY KEY, topic TEXT, content TEXT, utility REAL,
48
+ tags TEXT, created_at TEXT
49
+ )""")
50
+ c.execute("""CREATE TABLE IF NOT EXISTS tools (
51
+ name TEXT PRIMARY KEY, description TEXT, mode TEXT,
52
+ command_template TEXT, schema_data TEXT, enabled INTEGER,
53
+ created_at TEXT
54
+ )""")
55
+ c.execute("""CREATE TABLE IF NOT EXISTS sessions (
56
+ session_id TEXT PRIMARY KEY, workspace TEXT, cwd TEXT,
57
+ created_at TEXT, active INTEGER
58
+ )""")
59
+ conn.commit()
60
+ conn.close()
61
 
62
+ _init_db()
 
 
63
 
64
+ DB_LOCK = threading.Lock()
65
 
66
 
67
+ def _db():
68
+ conn = sqlite3.connect(str(DB_PATH), timeout=10)
69
  conn.row_factory = sqlite3.Row
70
  return conn
71
 
72
 
73
+ # ─── Helpers ──────────────────────────────────────────────────
74
+ def _ws_path(workspace: str, path: str = ".") -> Path:
75
+ base = WORKSPACE_ROOT / workspace
76
+ base.mkdir(parents=True, exist_ok=True)
77
+ resolved = (base / path).resolve()
78
+ if not str(resolved).startswith(str(base.resolve())):
79
+ raise HTTPException(status_code=400, detail="Path traversal denied")
80
+ return resolved
81
+
82
+
83
+ def _truncate(text: str, limit: int = 50000) -> tuple:
84
+ if len(text) > limit:
85
+ return text[:limit], True
86
+ return text, False
87
+
88
+
89
+ def _receipt(kind: str, summary: str, data: dict, workspace: Optional[str] = None) -> dict:
90
+ rid = uuid.uuid4().hex[:16]
91
+ sha = hashlib.sha256(json.dumps(data, sort_keys=True, default=str).encode()).hexdigest()
92
+ now = datetime.now(timezone.utc).isoformat()
93
+ with DB_LOCK:
94
+ conn = _db()
95
+ conn.execute(
96
+ "INSERT INTO receipts VALUES (?,?,?,?,?,?,?)",
97
+ (rid, kind, workspace, summary, sha, now, json.dumps(data, default=str)),
98
+ )
99
+ conn.commit()
100
+ conn.close()
101
+ return {"id": rid, "kind": kind, "workspace": workspace, "summary": summary, "sha256": sha, "created_at": now}
102
 
103
 
104
+ def _safe_run(cmd: list, cwd: Path, timeout: int) -> dict:
105
+ try:
106
+ result = subprocess.run(
107
+ cmd, cwd=str(cwd), capture_output=True, text=True, timeout=timeout,
108
+ )
109
+ stdout, truncated = _truncate(result.stdout)
110
+ stderr, _ = _truncate(result.stderr)
111
+ return {"returncode": result.returncode, "stdout": stdout, "stderr": stderr, "truncated": truncated}
112
+ except subprocess.TimeoutExpired:
113
+ return {"returncode": -1, "stdout": "", "stderr": f"Timed out after {timeout}s", "truncated": False}
114
+ except Exception as e:
115
+ return {"returncode": -1, "stdout": "", "stderr": str(e), "truncated": False}
116
 
117
 
118
+ # ═══════════════════════════════════════════════════════════════
119
+ # MODELS
120
+ # ═══════════════════════════════════════════════════════════════
121
+ class TerminalRunRequest(BaseModel):
122
+ workspace: str = "default"
123
+ command: str
124
+ timeout_seconds: int = Field(default=10, ge=1, le=30)
125
+ cwd: str = "."
126
+ create_receipt: bool = True
127
 
128
 
129
+ class PythonRunRequest(BaseModel):
130
+ workspace: str = "default"
131
+ code: str
132
+ timeout_seconds: int = Field(default=10, ge=1, le=30)
133
+ create_receipt: bool = True
134
 
135
 
136
+ class SessionCreateRequest(BaseModel):
137
+ workspace: str = "default"
138
+ cwd: str = "."
 
139
 
140
 
141
+ class SessionRunRequest(BaseModel):
142
+ session_id: str
143
+ command: str
144
+ timeout_seconds: int = Field(default=10, ge=1, le=30)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
 
 
 
 
146
 
147
+ class FileWriteRequest(BaseModel):
148
+ workspace: str = "default"
149
+ path: str
150
+ content: str
151
+ encoding: str = "utf-8"
152
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
+ class FileReadRequest(BaseModel):
155
+ workspace: str = "default"
156
+ path: str
157
+ max_bytes: int = Field(default=120000, ge=1, le=20000000)
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
+ class ListRequest(BaseModel):
161
+ workspace: str = "default"
162
+ path: str = "."
163
+ max_items: int = Field(default=250, ge=1, le=2000)
 
 
 
 
 
 
 
164
 
165
 
166
+ class ArtifactRequest(BaseModel):
167
+ workspace: str = "default"
168
+ filename: str
169
+ content: str
170
+ content_type: str = "text/plain"
 
 
 
 
 
171
 
172
 
173
+ class LearnRequest(BaseModel):
174
+ topic: str = "general"
175
+ content: str
176
+ utility: float = Field(default=0.5, ge=0, le=1)
177
+ tags: list = []
 
 
 
 
178
 
179
 
180
+ class RecallRequest(BaseModel):
181
+ query: str
182
+ limit: int = Field(default=8, ge=1, le=50)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
 
185
+ class ToolRegisterRequest(BaseModel):
186
+ name: str
187
+ description: str = ""
188
+ mode: str = "command"
189
+ command_template: str = ""
190
+ schema_data: dict = {}
191
+ enabled: bool = True
 
 
 
 
 
 
 
 
192
 
193
 
194
+ class ToolInvokeRequest(BaseModel):
195
+ workspace: str = "default"
196
+ args: dict = {}
197
+ timeout_seconds: int = Field(default=10, ge=1, le=30)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
 
200
+ # ═══════════════════════════════════════════════════════════════
201
+ # HEALTH
202
+ # ═══════════════════════════════════════════════════════════════
203
+ @app.get("/health")
204
+ async def health():
205
+ return {"status": "ok", "version": "3.1.1", "workspaces": len(list(WORKSPACE_ROOT.iterdir()))}
206
+
207
+
208
+ # ═══════════════════════════════════════════════════════════════
209
+ # TERMINAL RUN
210
+ # ═══════════════════════════════════════════════════════════════
211
+ @app.post("/terminal/run")
212
+ async def run_terminal(req: TerminalRunRequest):
213
+ cwd = _ws_path(req.workspace, req.cwd)
214
+ result = _safe_run(["sh", "-c", req.command], cwd, req.timeout_seconds)
215
+ receipt = None
216
+ receipt_error = None
217
+ if req.create_receipt:
218
+ try:
219
+ receipt = _receipt("terminal_run", req.command[:80], result, req.workspace)
220
+ except Exception as e:
221
+ receipt_error = str(e)
222
+ return {"workspace": req.workspace, "cwd": req.cwd, "command": req.command, **result, "receipt": receipt, "receipt_error": receipt_error}
223
+
224
+
225
+ # ════════════════���══════════════════════════════════════════════
226
+ # PYTHON RUN
227
+ # ═══════════════════════════════════════════════════════════════
228
+ @app.post("/python/run")
229
+ async def run_python(req: PythonRunRequest):
230
+ ws_base = _ws_path(req.workspace)
231
+ script_path = ws_base / f"_run_{uuid.uuid4().hex[:8]}.py"
232
+ script_path.write_text(req.code)
233
+ result = _safe_run(["python3", str(script_path)], ws_base, req.timeout_seconds)
234
+ try:
235
+ script_path.unlink(missing_ok=True)
236
+ except Exception:
237
+ pass
238
+ receipt = None
239
+ receipt_error = None
240
+ if req.create_receipt:
241
+ try:
242
+ receipt = _receipt("python_run", req.code[:80], result, req.workspace)
243
+ except Exception as e:
244
+ receipt_error = str(e)
245
+ return {"workspace": req.workspace, "script": script_path.name, **result, "receipt": receipt, "receipt_error": receipt_error}
246
+
247
+
248
+ # ═══════════════════════════════════════════════════════════════
249
+ # SESSIONS
250
+ # ═══════════════════════════════════════════════════════════════
251
+ @app.post("/session/create")
252
+ async def create_session(req: SessionCreateRequest):
253
+ sid = uuid.uuid4().hex[:12]
254
+ _ws_path(req.workspace, req.cwd)
255
+ now = datetime.now(timezone.utc).isoformat()
256
+ with DB_LOCK:
257
+ conn = _db()
258
+ conn.execute("INSERT INTO sessions VALUES (?,?,?,?,1)", (sid, req.workspace, req.cwd, now))
259
+ conn.commit()
260
+ conn.close()
261
+ return {"session_id": sid, "workspace": req.workspace, "cwd": req.cwd, "created_at": now}
262
 
263
 
264
+ @app.post("/session/run")
265
+ async def run_session_command(req: SessionRunRequest):
266
+ with DB_LOCK:
267
+ conn = _db()
268
+ row = conn.execute("SELECT * FROM sessions WHERE session_id=? AND active=1", (req.session_id,)).fetchone()
269
+ conn.close()
270
+ if not row:
271
+ raise HTTPException(status_code=404, detail="Session not found or inactive")
272
+ cwd = _ws_path(row["workspace"], row["cwd"])
273
+ result = _safe_run(["sh", "-c", req.command], cwd, req.timeout_seconds)
274
+ return {"workspace": row["workspace"], "cwd": row["cwd"], "command": req.command, **result, "receipt": _receipt("session_run", req.command[:80], result, row["workspace"]), "receipt_error": None}
275
+
276
+
277
+ # ═══════════════════════════════════════════════════════════════
278
+ # FILES
279
+ # ═══════════════════════════════════════════════════════════════
280
+ @app.post("/files/write")
281
+ async def write_file(req: FileWriteRequest):
282
+ target = _ws_path(req.workspace, req.path)
283
+ target.parent.mkdir(parents=True, exist_ok=True)
284
+ target.write_text(req.content, encoding=req.encoding)
285
+ receipt = _receipt("file_write", req.path, {"bytes": len(req.content)}, req.workspace)
286
+ return {"status": "written", "path": req.path, "bytes": len(req.content), "receipt": receipt}
287
+
288
+
289
+ @app.post("/files/read")
290
+ async def read_file(req: FileReadRequest):
291
+ target = _ws_path(req.workspace, req.path)
292
+ if not target.exists():
293
+ raise HTTPException(status_code=404, detail="File not found")
294
+ if target.is_dir():
295
+ raise HTTPException(status_code=400, detail="Path is a directory")
296
+ data = target.read_bytes()[:req.max_bytes]
297
+ try:
298
+ text = data.decode("utf-8")
299
+ return {"path": req.path, "content": text, "bytes": len(data), "truncated": target.stat().st_size > req.max_bytes}
300
+ except UnicodeDecodeError:
301
+ import base64
302
+ return {"path": req.path, "content_base64": base64.b64encode(data).decode(), "bytes": len(data), "truncated": target.stat().st_size > req.max_bytes}
303
+
304
+
305
+ @app.post("/files/list")
306
+ async def list_files(req: ListRequest):
307
+ target = _ws_path(req.workspace, req.path)
308
+ if not target.exists():
309
+ return {"path": req.path, "entries": []}
310
+ entries = []
311
+ if target.is_dir():
312
+ for item in sorted(target.iterdir())[:req.max_items]:
313
+ entries.append({"name": item.name, "type": "dir" if item.is_dir() else "file", "size": item.stat().st_size if item.is_file() else None})
314
+ return {"path": req.path, "entries": entries}
315
+
316
+
317
+ @app.get("/workspace/tree")
318
+ async def get_workspace_tree(workspace: str = "default", max_items: int = 500):
319
+ base = _ws_path(workspace)
320
+ lines = []
321
+ count = 0
322
+ for p in sorted(base.rglob("*")):
323
+ if count >= max_items:
324
+ lines.append("... (truncated)")
325
+ break
326
+ rel = p.relative_to(base)
327
+ indent = " " * (len(rel.parts) - 1)
328
+ marker = "/" if p.is_dir() else ""
329
+ lines.append(f"{indent}{p.name}{marker}")
330
+ count += 1
331
+ return PlainTextResponse("\n".join(lines) if lines else "(empty)")
332
+
333
+
334
+ # ═══════════════════════════════════════════════════════════════
335
+ # ARTIFACTS
336
+ # ═══════════════════════════════════════════════════════════════
337
+ @app.post("/artifact/compile")
338
+ async def compile_artifact(req: ArtifactRequest):
339
+ artifact_hash = hashlib.sha256(req.content.encode()).hexdigest()[:16]
340
+ artifact_dir = ARTIFACTS_DIR / artifact_hash
341
+ artifact_dir.mkdir(parents=True, exist_ok=True)
342
+ (artifact_dir / req.filename).write_text(req.content)
343
+ (artifact_dir / "meta.json").write_text(json.dumps({"filename": req.filename, "content_type": req.content_type, "workspace": req.workspace, "sha256": artifact_hash, "created_at": datetime.now(timezone.utc).isoformat()}, indent=2))
344
+ receipt = _receipt("artifact", req.filename, {"hash": artifact_hash}, req.workspace)
345
+ return {"hash": artifact_hash, "filename": req.filename, "url": f"/artifact/{artifact_hash}/{req.filename}", "receipt": receipt}
346
+
347
+
348
+ @app.post("/url/issue")
349
+ async def issue_url(req: ArtifactRequest):
350
+ artifact_hash = hashlib.sha256(req.content.encode()).hexdigest()[:16]
351
+ artifact_dir = ARTIFACTS_DIR / artifact_hash
352
+ artifact_dir.mkdir(parents=True, exist_ok=True)
353
+ (artifact_dir / req.filename).write_text(req.content)
354
+ url = f"https://josephrw-endpoint.hf.space/artifact/{artifact_hash}/{req.filename}"
355
+ receipt = _receipt("url_issue", req.filename, {"url": url, "hash": artifact_hash}, req.workspace)
356
+ return {"url": url, "hash": artifact_hash, "filename": req.filename, "receipt": receipt}
357
+
358
+
359
+ # ═══════════════════════════════════════════════════════════════
360
+ # MEMORY
361
+ # ═══════════════════════════════════════════════════════════════
362
+ @app.post("/learn")
363
+ async def learn_memory(req: LearnRequest):
364
+ mid = uuid.uuid4().hex[:16]
365
+ now = datetime.now(timezone.utc).isoformat()
366
+ with DB_LOCK:
367
+ conn = _db()
368
+ conn.execute("INSERT INTO memory VALUES (?,?,?,?,?,?)", (mid, req.topic, req.content, req.utility, json.dumps(req.tags), now))
369
+ conn.commit()
370
+ conn.close()
371
+ return {"status": "learned", "id": mid, "topic": req.topic}
372
 
373
 
374
+ @app.post("/recall")
375
+ async def recall_memory(req: RecallRequest):
376
+ with DB_LOCK:
377
+ conn = _db()
378
+ rows = conn.execute("SELECT * FROM memory WHERE content LIKE ? OR topic LIKE ? ORDER BY utility DESC LIMIT ?", (f"%{req.query}%", f"%{req.query}%", req.limit)).fetchall()
379
+ conn.close()
380
+ return {"results": [{"id": r["id"], "topic": r["topic"], "content": r["content"], "utility": r["utility"], "tags": json.loads(r["tags"]), "created_at": r["created_at"]} for r in rows]}
381
+
382
+
383
+ # ═══════════════════════════════════════════════════════════════
384
+ # DYNAMIC TOOLS
385
+ # ═══════════════════════════════════════════════════════════════
386
+ @app.post("/tool/register")
387
+ async def register_tool(req: ToolRegisterRequest):
388
+ now = datetime.now(timezone.utc).isoformat()
389
+ with DB_LOCK:
390
+ conn = _db()
391
+ conn.execute("INSERT OR REPLACE INTO tools VALUES (?,?,?,?,?,?,?)", (req.name, req.description, req.mode, req.command_template, json.dumps(req.schema_data), int(req.enabled), now))
392
+ conn.commit()
393
+ conn.close()
394
+ return {"status": "registered", "name": req.name, "mode": req.mode}
395
 
396
 
397
+ @app.get("/tools")
398
+ async def list_tools():
399
+ with DB_LOCK:
400
+ conn = _db()
401
+ rows = conn.execute("SELECT * FROM tools WHERE enabled=1").fetchall()
402
+ conn.close()
403
+ return {"tools": [{"name": r["name"], "description": r["description"], "mode": r["mode"], "command_template": r["command_template"], "schema": json.loads(r["schema_data"])} for r in rows]}
 
 
 
404
 
405
 
406
+ @app.post("/tool/{name}")
407
+ async def invoke_tool(name: str, req: ToolInvokeRequest):
408
+ with DB_LOCK:
409
+ conn = _db()
410
+ row = conn.execute("SELECT * FROM tools WHERE name=? AND enabled=1", (name,)).fetchone()
411
+ conn.close()
412
+ if not row:
413
+ raise HTTPException(status_code=404, detail=f"Tool '{name}' not found")
414
+ template = row["command_template"]
415
+ cmd = template
416
+ for k, v in req.args.items():
417
+ cmd = cmd.replace(f"{{{{{k}}}}}", str(v))
418
+ if row["mode"] == "python":
419
+ ws_base = _ws_path(req.workspace)
420
+ script = ws_base / f"_tool_{uuid.uuid4().hex[:8]}.py"
421
+ script.write_text(cmd)
422
+ result = _safe_run(["python3", str(script)], ws_base, req.timeout_seconds)
423
+ script.unlink(missing_ok=True)
424
+ else:
425
+ cwd = _ws_path(req.workspace)
426
+ result = _safe_run(["sh", "-c", cmd], cwd, req.timeout_seconds)
427
+ receipt = _receipt("tool_invoke", name, result, req.workspace)
428
+ return {"tool": name, **result, "receipt": receipt}
429
+
430
+
431
+ # ═══════════════════════════════════════════════════════════════
432
+ # RECEIPTS / LEDGER
433
+ # ═══════════════════════════════════════════════════════════════
434
+ @app.get("/ledger/recent")
435
+ async def get_recent_ledger(limit: int = 50):
436
+ with DB_LOCK:
437
+ conn = _db()
438
+ rows = conn.execute("SELECT id, kind, workspace, summary, sha256, created_at FROM receipts ORDER BY created_at DESC LIMIT ?", (limit,)).fetchall()
439
+ conn.close()
440
+ return {"receipts": [{"id": r["id"], "kind": r["kind"], "workspace": r["workspace"], "summary": r["summary"], "sha256": r["sha256"], "created_at": r["created_at"]} for r in rows]}
441
 
442
 
443
+ @app.get("/receipt/{receipt_id}")
444
+ async def get_receipt(receipt_id: str):
445
+ with DB_LOCK:
446
+ conn = _db()
447
+ row = conn.execute("SELECT * FROM receipts WHERE id=?", (receipt_id,)).fetchone()
448
+ conn.close()
449
+ if not row:
450
+ raise HTTPException(status_code=404, detail="Receipt not found")
451
+ return {"id": row["id"], "kind": row["kind"], "workspace": row["workspace"], "summary": row["summary"], "sha256": row["sha256"], "created_at": row["created_at"], "data": json.loads(row["data_json"])}
 
 
 
 
 
 
 
 
 
 
452
 
453
 
454
+ # ═══════════════════════════════════════════════════════════════
455
+ # STATIC ARTIFACT SERVING (must be last — catches /artifact/* GETs)
456
+ # ═══════════════════════════════════════════════════════════════
457
+ app.mount("/artifact", StaticFiles(directory=str(ARTIFACTS_DIR)), name="artifacts")
 
 
 
 
 
 
 
 
 
 
 
458
 
459
 
460
+ # ═══════════════════════════════════════════════════════════════
461
+ # ROOT
462
+ # ═══════════════════════════════════════════════════════════════
463
+ @app.get("/")
464
+ async def root():
 
 
 
 
 
 
465
  return {
466
+ "service": "Giant GPT Terminal Kernel",
467
+ "version": "3.1.1",
468
+ "endpoints": [
469
+ "GET /health",
470
+ "POST /terminal/run",
471
+ "POST /python/run",
472
+ "POST /session/create",
473
+ "POST /session/run",
474
+ "POST /files/write",
475
+ "POST /files/read",
476
+ "POST /files/list",
477
+ "GET /workspace/tree",
478
+ "POST /artifact/compile",
479
+ "POST /url/issue",
480
+ "POST /learn",
481
+ "POST /recall",
482
+ "POST /tool/register",
483
+ "GET /tools",
484
+ "POST /tool/{name}",
485
+ "GET /ledger/recent",
486
+ "GET /receipt/{receipt_id}",
487
+ ],
488
  }
 
 
 
 
 
data/artifacts/290f663cc2ed7fec/data.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"status":"live"}
data/content_pool.db DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:fb715937575fa2e102eaf8f3d93e2ffa743619929cf355a042e3da5246d2644c
3
- size 225280
 
 
 
 
data/masseurs.db DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:751f7531cdb8e6d02d1a3d54bc96165a4293acfade1f1df474e5b40fbf6386f5
3
- size 6619136
 
 
 
 
data/unified.db DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:e47431dbbbf3d37d0a62bc4f65fe6c0c15c452a88cecdbd7a562abd6cce8187a
3
- size 1658880
 
 
 
 
data/workspaces/default/test/hello.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ GPT Actions rock!
templates/control.html DELETED
@@ -1,271 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>MasseurBoost — Orchestrator Control Panel</title>
7
- <style>
8
- :root {
9
- --bg: #0a0e17; --card: #131825; --border: #1e2a3d; --text: #e4e7ee;
10
- --muted: #6b7488; --accent: #4f9eff; --green: #22c55e; --red: #ef4444;
11
- --yellow: #eab923; --orange: #f97316; --purple: #a78bfa;
12
- }
13
- * { margin: 0; padding: 0; box-sizing: border-box; }
14
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; }
15
-
16
- .header { background: var(--card); border-bottom: 1px solid var(--border); padding: 16px 24px; display: flex; align-items: center; justify-content: space-between; }
17
- .header h1 { font-size: 20px; font-weight: 700; }
18
- .header h1 span { color: var(--accent); }
19
- .header nav a { color: var(--muted); text-decoration: none; margin-left: 20px; font-size: 14px; }
20
- .header nav a:hover { color: var(--text); }
21
- .header nav a.active { color: var(--accent); }
22
-
23
- .container { max-width: 1280px; margin: 0 auto; padding: 24px; }
24
-
25
- /* Daemon status bar */
26
- .daemon-bar { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 20px 24px; margin-bottom: 24px; display: flex; align-items: center; justify-content: space-between; }
27
- .daemon-status { display: flex; align-items: center; gap: 12px; }
28
- .daemon-dot { width: 12px; height: 12px; border-radius: 50%; background: var(--red); transition: background 0.3s; }
29
- .daemon-dot.running { background: var(--green); box-shadow: 0 0 8px var(--green); animation: pulse 2s infinite; }
30
- @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
31
- .daemon-text { font-size: 16px; font-weight: 600; }
32
- .daemon-text small { color: var(--muted); font-weight: 400; font-size: 13px; margin-left: 8px; }
33
- .btn { padding: 8px 18px; border-radius: 8px; border: none; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s; }
34
- .btn-start { background: var(--green); color: #fff; }
35
- .btn-stop { background: var(--red); color: #fff; }
36
- .btn:hover { opacity: 0.85; transform: translateY(-1px); }
37
- .btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
38
-
39
- /* Stats grid */
40
- .stats-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 16px; margin-bottom: 24px; }
41
- .stat-card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 18px; text-align: center; }
42
- .stat-card .num { font-size: 28px; font-weight: 700; color: var(--accent); }
43
- .stat-card .label { font-size: 12px; color: var(--muted); margin-top: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
44
-
45
- /* Task table */
46
- .section-title { font-size: 18px; font-weight: 700; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
47
- .section-title .badge { font-size: 11px; padding: 2px 8px; border-radius: 4px; background: var(--border); color: var(--muted); }
48
-
49
- .task-table { background: var(--card); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; margin-bottom: 24px; }
50
- .task-row { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr 0.8fr 0.8fr; padding: 14px 20px; border-bottom: 1px solid var(--border); align-items: center; font-size: 14px; }
51
- .task-row:last-child { border-bottom: none; }
52
- .task-row.header-row { font-weight: 600; color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
53
- .task-row:hover { background: rgba(79, 158, 255, 0.05); }
54
-
55
- .task-name { font-weight: 600; }
56
- .task-name .icon { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 8px; }
57
- .task-name .icon.due { background: var(--yellow); }
58
- .task-name .icon.not-due { background: var(--green); }
59
- .task-name .icon.running { background: var(--accent); animation: pulse 1s infinite; }
60
-
61
- .status-pill { padding: 3px 10px; border-radius: 12px; font-size: 11px; font-weight: 600; text-transform: uppercase; }
62
- .status-pill.due { background: rgba(234, 185, 35, 0.15); color: var(--yellow); }
63
- .status-pill.ok { background: rgba(34, 197, 94, 0.15); color: var(--green); }
64
- .status-pill.running { background: rgba(79, 158, 255, 0.15); color: var(--accent); }
65
- .status-pill.idle { background: rgba(107, 116, 136, 0.15); color: var(--muted); }
66
-
67
- .btn-sm { padding: 5px 12px; border-radius: 6px; font-size: 12px; }
68
- .btn-trigger { background: var(--accent); color: #fff; }
69
- .btn-trigger:disabled { background: var(--border); color: var(--muted); }
70
-
71
- /* Log feed */
72
- .log-feed { background: #0d1117; border: 1px solid var(--border); border-radius: 12px; padding: 16px; max-height: 400px; overflow-y: auto; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; }
73
- .log-entry { padding: 4px 0; border-bottom: 1px solid rgba(30, 42, 61, 0.5); display: flex; gap: 12px; }
74
- .log-entry .ts { color: var(--muted); white-space: nowrap; }
75
- .log-entry .comp { color: var(--purple); min-width: 140px; }
76
- .log-entry .status { font-weight: 600; }
77
- .log-entry .status.success { color: var(--green); }
78
- .log-entry .status.failed { color: var(--red); }
79
- .log-entry .status.started { color: var(--accent); }
80
- .log-entry .status.stopped { color: var(--orange); }
81
-
82
- /* Run All bar */
83
- .run-all-bar { display: flex; gap: 12px; margin-bottom: 24px; }
84
- .btn-run-all { background: var(--purple); color: #fff; padding: 12px 28px; font-size: 15px; }
85
-
86
- /* Responsive */
87
- @media (max-width: 768px) {
88
- .task-row { grid-template-columns: 1fr; gap: 8px; }
89
- .task-row.header-row { display: none; }
90
- .stats-grid { grid-template-columns: repeat(2, 1fr); }
91
- }
92
- </style>
93
- </head>
94
- <body>
95
-
96
- <div class="header">
97
- <h1>Masseur<span>Boost</span> Control Panel</h1>
98
- <nav>
99
- <a href="/">Home</a>
100
- <a href="/dashboard">Market Intel</a>
101
- <a href="/control" class="active">Orchestrator</a>
102
- </nav>
103
- </div>
104
-
105
- <div class="container">
106
-
107
- <!-- Daemon Status -->
108
- <div class="daemon-bar">
109
- <div class="daemon-status">
110
- <div class="daemon-dot" id="daemonDot"></div>
111
- <div class="daemon-text" id="daemonText">Checking... <small id="daemonSub"></small></div>
112
- </div>
113
- <div>
114
- <button class="btn btn-start" id="daemonStart" onclick="daemonAction('start')">Start Daemon</button>
115
- <button class="btn btn-stop" id="daemonStop" onclick="daemonAction('stop')" style="display:none">Stop Daemon</button>
116
- </div>
117
- </div>
118
-
119
- <!-- DB Stats -->
120
- <div class="section-title">Database Stats</div>
121
- <div class="stats-grid" id="statsGrid">
122
- <div class="stat-card"><div class="num">—</div><div class="label">Loading</div></div>
123
- </div>
124
-
125
- <!-- Run All -->
126
- <div class="run-all-bar">
127
- <button class="btn btn-run-all" onclick="triggerTask('all')">Run Full Pipeline Now</button>
128
- </div>
129
-
130
- <!-- Task Schedule -->
131
- <div class="section-title">Pipeline Tasks <span class="badge" id="taskCount">—</span></div>
132
- <div class="task-table" id="taskTable">
133
- <div class="task-row header-row">
134
- <div>Task</div>
135
- <div>Interval</div>
136
- <div>Last Run</div>
137
- <div>Next In</div>
138
- <div>Status</div>
139
- <div>Action</div>
140
- </div>
141
- <div id="taskRows">
142
- <div class="task-row"><div style="color:var(--muted)">Loading tasks...</div></div>
143
- </div>
144
- </div>
145
-
146
- <!-- Live Log Feed -->
147
- <div class="section-title">Live Orchestrator Log</div>
148
- <div class="log-feed" id="logFeed">
149
- <div style="color:var(--muted)">Loading logs...</div>
150
- </div>
151
-
152
- </div>
153
-
154
- <script>
155
- const API = '/api/orchestrator';
156
-
157
- async function fetchStatus() {
158
- try {
159
- const r = await fetch(`${API}/status`);
160
- const d = await r.json();
161
- updateDaemon(d.daemon_running);
162
- renderTasks(d.tasks);
163
- renderLogs(d.log_tail);
164
- } catch (e) {
165
- updateDaemon(false);
166
- }
167
- }
168
-
169
- async function fetchStats() {
170
- try {
171
- const r = await fetch(`${API}/db-stats`);
172
- const d = await r.json();
173
- const items = [
174
- ['masseurs_scraped', 'Masseurs Scraped'],
175
- ['cities_scraped', 'Cities'],
176
- ['visitors', 'Visitors Tracked'],
177
- ['visit_log', 'Visit Actions'],
178
- ['hourly_kpis', 'KPI Snapshots'],
179
- ['profile_stats', 'Profile Stats'],
180
- ['bio_experiments', 'Bio Experiments'],
181
- ['blog_posts', 'Blog Posts'],
182
- ['interview_sets', 'Interview Sets'],
183
- ['content_variants', 'Content Variants'],
184
- ['competitor_benchmarks', 'Benchmarks'],
185
- ['receipts', 'Receipts Logged'],
186
- ];
187
- document.getElementById('statsGrid').innerHTML = items.map(([k, label]) =>
188
- `<div class="stat-card"><div class="num">${d[k] != null ? d[k].toLocaleString() : '—'}</div><div class="label">${label}</div></div>`
189
- ).join('');
190
- } catch (e) {}
191
- }
192
-
193
- function updateDaemon(running) {
194
- const dot = document.getElementById('daemonDot');
195
- const text = document.getElementById('daemonText');
196
- const sub = document.getElementById('daemonSub');
197
- const startBtn = document.getElementById('daemonStart');
198
- const stopBtn = document.getElementById('daemonStop');
199
- if (running) {
200
- dot.classList.add('running');
201
- text.innerHTML = 'Daemon Running <small>24/7 pipeline active</small>';
202
- startBtn.style.display = 'none';
203
- stopBtn.style.display = 'inline-block';
204
- } else {
205
- dot.classList.remove('running');
206
- text.innerHTML = 'Daemon Stopped <small>Click Start to run 24/7</small>';
207
- startBtn.style.display = 'inline-block';
208
- stopBtn.style.display = 'none';
209
- }
210
- }
211
-
212
- function renderTasks(tasks) {
213
- document.getElementById('taskCount').textContent = `${tasks.length} tasks`;
214
- document.getElementById('taskRows').innerHTML = tasks.map(t => {
215
- const iconClass = t.running ? 'running' : (t.is_due ? 'due' : 'not-due');
216
- const pillClass = t.running ? 'running' : (t.is_due ? 'due' : 'ok');
217
- const pillText = t.running ? 'RUNNING' : (t.is_due ? 'DUE' : 'OK');
218
- const lastRun = t.last_run ? new Date(t.last_run).toLocaleString('en-US', {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}) : 'Never';
219
- const nextIn = t.next_in_hours != null ? (t.next_in_hours <= 0 ? 'Now' : `${t.next_in_hours}h`) : '—';
220
- return `<div class="task-row">
221
- <div class="task-name"><span class="icon ${iconClass}"></span>${t.label}</div>
222
- <div>${t.interval_hours}h</div>
223
- <div style="color:var(--muted);font-size:13px">${lastRun}</div>
224
- <div>${nextIn}</div>
225
- <div><span class="status-pill ${pillClass}">${pillText}</span></div>
226
- <div><button class="btn btn-sm btn-trigger" onclick="triggerTask('${t.task}')" ${t.running ? 'disabled' : ''}>Run</button></div>
227
- </div>`;
228
- }).join('');
229
- }
230
-
231
- function renderLogs(logs) {
232
- if (!logs || !logs.length) {
233
- document.getElementById('logFeed').innerHTML = '<div style="color:var(--muted)">No logs yet</div>';
234
- return;
235
- }
236
- document.getElementById('logFeed').innerHTML = logs.reverse().map(l => {
237
- const ts = l.timestamp ? new Date(l.timestamp).toLocaleTimeString('en-US', {hour12:false}) : '—';
238
- const comp = l.component || '—';
239
- const status = l.status || '';
240
- const statusClass = status === 'success' ? 'success' : status === 'failed' ? 'failed' : status === 'started' ? 'started' : status === 'stopped' ? 'stopped' : '';
241
- return `<div class="log-entry"><span class="ts">${ts}</span><span class="comp">${comp}</span><span class="status ${statusClass}">${status}</span></div>`;
242
- }).join('');
243
- }
244
-
245
- async function triggerTask(task) {
246
- const btn = event?.target;
247
- if (btn) { btn.disabled = true; btn.textContent = 'Running...'; }
248
- try {
249
- await fetch(`${API}/trigger/${task}`, { method: 'POST' });
250
- setTimeout(() => { fetchStatus(); if (btn) { btn.disabled = false; btn.textContent = 'Run'; } }, 1000);
251
- } catch (e) {
252
- if (btn) { btn.disabled = false; btn.textContent = 'Run'; }
253
- }
254
- }
255
-
256
- async function daemonAction(action) {
257
- try {
258
- await fetch(`${API}/daemon/${action}`, { method: 'POST' });
259
- setTimeout(fetchStatus, 2000);
260
- } catch (e) {}
261
- }
262
-
263
- // Auto-refresh
264
- fetchStatus();
265
- fetchStats();
266
- setInterval(fetchStatus, 5000);
267
- setInterval(fetchStats, 30000);
268
- </script>
269
-
270
- </body>
271
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
templates/dashboard.html DELETED
@@ -1,517 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
-
4
- <head>
5
- <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>MasseurBoost Dashboard</title>
8
- <script src="https://cdn.tailwindcss.com"></script>
9
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap"
10
- rel="stylesheet">
11
- <style>
12
- * {
13
- font-family: 'Inter', sans-serif;
14
- }
15
-
16
- .gradient-bg {
17
- background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%);
18
- }
19
-
20
- .glow {
21
- box-shadow: 0 0 30px rgba(139, 92, 246, 0.2);
22
- }
23
-
24
- .card {
25
- background: rgba(255, 255, 255, 0.03);
26
- border: 1px solid rgba(255, 255, 255, 0.08);
27
- border-radius: 1.5rem;
28
- }
29
-
30
- .card-hover {
31
- transition: all 0.3s;
32
- }
33
-
34
- .card-hover:hover {
35
- transform: translateY(-4px);
36
- border-color: rgba(139, 92, 246, 0.3);
37
- }
38
-
39
- .pulse-dot {
40
- animation: pulse-dot 2s infinite;
41
- }
42
-
43
- @keyframes pulse-dot {
44
-
45
- 0%,
46
- 100% {
47
- opacity: 1;
48
- }
49
-
50
- 50% {
51
- opacity: 0.3;
52
- }
53
- }
54
-
55
- .tab-active {
56
- background: linear-gradient(135deg, #8b5cf6, #d946ef);
57
- }
58
-
59
- .progress-bar {
60
- background: linear-gradient(90deg, #8b5cf6, #d946ef, #ec4899);
61
- }
62
-
63
- .spin {
64
- animation: spin 2s linear infinite;
65
- }
66
-
67
- @keyframes spin {
68
- to {
69
- transform: rotate(360deg);
70
- }
71
- }
72
- </style>
73
- </head>
74
-
75
- <body class="bg-[#0a0a0f] text-white min-h-screen">
76
-
77
- <!-- Top bar -->
78
- <div class="fixed top-0 w-full z-50 bg-[#0a0a0f]/80 backdrop-blur-lg border-b border-white/5">
79
- <div class="max-w-7xl mx-auto px-6 py-3 flex items-center justify-between">
80
- <div class="flex items-center gap-3">
81
- <a href="/" class="flex items-center gap-2">
82
- <div
83
- class="w-9 h-9 rounded-xl bg-gradient-to-br from-violet-500 to-fuchsia-500 flex items-center justify-center font-black">
84
- M</div>
85
- <span class="font-bold">Masseur<span class="text-violet-400">Boost</span></span>
86
- </a>
87
- <span class="text-gray-600">/</span>
88
- <span class="text-gray-400 text-sm">Dashboard</span>
89
- </div>
90
- <div class="flex items-center gap-4">
91
- <div class="flex items-center gap-2 text-sm">
92
- <span class="w-2 h-2 rounded-full bg-green-400 pulse-dot"></span>
93
- <span class="text-gray-400">Engine: Active</span>
94
- </div>
95
- <div class="text-sm text-gray-500">Cycle: <span id="cycle-time" class="text-violet-400">3h</span></div>
96
- </div>
97
- </div>
98
- </div>
99
-
100
- <!-- Main -->
101
- <div class="pt-20 max-w-7xl mx-auto px-6 py-8">
102
-
103
- <!-- Header -->
104
- <div class="mb-8">
105
- <h1 class="text-3xl font-black mb-2">Optimization Dashboard</h1>
106
- <p class="text-gray-500">Real-time view of your profile's AI optimization engine</p>
107
- </div>
108
-
109
- <!-- Stats row -->
110
- <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8" id="stats-row">
111
- <div class="card p-6">
112
- <div class="text-sm text-gray-500 mb-1">Market Size</div>
113
- <div class="text-3xl font-black text-violet-400" id="stat-total">—</div>
114
- <div class="text-xs text-gray-600 mt-1">masseurs tracked</div>
115
- </div>
116
- <div class="card p-6">
117
- <div class="text-sm text-gray-500 mb-1">Cities</div>
118
- <div class="text-3xl font-black text-fuchsia-400" id="stat-cities">—</div>
119
- <div class="text-xs text-gray-600 mt-1">worldwide</div>
120
- </div>
121
- <div class="card p-6">
122
- <div class="text-sm text-gray-500 mb-1">Avg Views/Day</div>
123
- <div class="text-3xl font-black text-pink-400" id="stat-avgvpd">—</div>
124
- <div class="text-xs text-gray-600 mt-1">market average</div>
125
- </div>
126
- <div class="card p-6">
127
- <div class="text-sm text-gray-500 mb-1">Gold Members</div>
128
- <div class="text-3xl font-black text-amber-400" id="stat-gold">—</div>
129
- <div class="text-xs text-gray-600 mt-1">of total</div>
130
- </div>
131
- </div>
132
-
133
- <!-- Tabs -->
134
- <div class="flex gap-2 mb-8 overflow-x-auto">
135
- <button onclick="switchTab('analyze')" id="tab-analyze"
136
- class="tab-active px-5 py-2.5 rounded-full text-sm font-semibold whitespace-nowrap">🔍 Profile
137
- Analysis</button>
138
- <button onclick="switchTab('bio')" id="tab-bio"
139
- class="px-5 py-2.5 rounded-full text-sm font-semibold whitespace-nowrap bg-white/5 border border-white/10 hover:bg-white/10 transition">🧬
140
- Bio Optimizer</button>
141
- <button onclick="switchTab('blog')" id="tab-blog"
142
- class="px-5 py-2.5 rounded-full text-sm font-semibold whitespace-nowrap bg-white/5 border border-white/10 hover:bg-white/10 transition">✍️
143
- Blog Generator</button>
144
- <button onclick="switchTab('photo')" id="tab-photo"
145
- class="px-5 py-2.5 rounded-full text-sm font-semibold whitespace-nowrap bg-white/5 border border-white/10 hover:bg-white/10 transition">📸
146
- Photo Rotation</button>
147
- <button onclick="switchTab('gravity')" id="tab-gravity"
148
- class="px-5 py-2.5 rounded-full text-sm font-semibold whitespace-nowrap bg-white/5 border border-white/10 hover:bg-white/10 transition">🌍
149
- Availability Gravity</button>
150
- <button onclick="switchTab('engine')" id="tab-engine"
151
- class="px-5 py-2.5 rounded-full text-sm font-semibold whitespace-nowrap bg-white/5 border border-white/10 hover:bg-white/10 transition">⚙️
152
- Engine Status</button>
153
- <button onclick="switchTab('search')" id="tab-search"
154
- class="px-5 py-2.5 rounded-full text-sm font-semibold whitespace-nowrap bg-white/5 border border-white/10 hover:bg-white/10 transition">🔎
155
- Browse Market</button>
156
- </div>
157
-
158
- <!-- Tab: Analyze -->
159
- <div id="panel-analyze" class="tab-panel">
160
- <div class="card p-8 mb-6">
161
- <h2 class="text-2xl font-bold mb-4">Analyze Your Profile</h2>
162
- <p class="text-gray-400 mb-6">Enter your RentMasseur username to see how you rank against all 2,738
163
- masseurs worldwide.</p>
164
- <div class="flex gap-3 mb-6">
165
- <input id="analyze-username" type="text" placeholder="Your RentMasseur username"
166
- class="flex-1 px-5 py-3 rounded-xl bg-white/5 border border-white/10 focus:border-violet-500 outline-none text-white">
167
- <button onclick="analyzeProfile()"
168
- class="px-6 py-3 rounded-xl bg-gradient-to-r from-violet-500 to-fuchsia-500 font-semibold hover:opacity-90 transition">Analyze</button>
169
- </div>
170
- </div>
171
- <div id="analyze-results" class="hidden"></div>
172
- </div>
173
-
174
- <!-- Tab: Bio -->
175
- <div id="panel-bio" class="tab-panel hidden">
176
- <div class="card p-8 mb-6">
177
- <h2 class="text-2xl font-bold mb-4">🧬 GA/RL Bio Optimizer</h2>
178
- <p class="text-gray-400 mb-6">Generate 30 bio variants using Genetic Algorithm + LLM. Each variant is
179
- scored on CTA strength, urgency, emotional hook, SEO, and uniqueness. The winner gets pushed to your
180
- live profile.</p>
181
- <div class="grid md:grid-cols-2 gap-6 mb-6">
182
- <div>
183
- <label class="text-sm text-gray-400 mb-2 block">Your username</label>
184
- <input id="bio-username" type="text" placeholder="e.g. Karpathianwolf"
185
- class="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 focus:border-violet-500 outline-none">
186
- </div>
187
- <div>
188
- <label class="text-sm text-gray-400 mb-2 block">Bio style</label>
189
- <select id="bio-style"
190
- class="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 focus:border-violet-500 outline-none">
191
- <option value="balanced">Balanced (recommended)</option>
192
- <option value="funny">Funny & Engaging</option>
193
- <option value="luxury">Luxury / Concierge</option>
194
- <option value="clinical">Clinical / Professional</option>
195
- <option value="mysterious">Mysterious & Alluring</option>
196
- </select>
197
- </div>
198
- </div>
199
- <button onclick="generateBio()"
200
- class="px-8 py-3 rounded-xl bg-gradient-to-r from-violet-500 to-fuchsia-500 font-semibold hover:opacity-90 transition">Generate
201
- Optimized Bio</button>
202
- </div>
203
- <div id="bio-results" class="hidden"></div>
204
- </div>
205
-
206
- <!-- Tab: Blog -->
207
- <div id="panel-blog" class="tab-panel hidden">
208
- <div class="card p-8 mb-6">
209
- <h2 class="text-2xl font-bold mb-4">✍️ LLM Blog Generator</h2>
210
- <p class="text-gray-400 mb-6">SEO-optimized blog posts generated from winning competitor patterns.
211
- Funny, engaging, and engineered to drive phone calls and bookings.</p>
212
- <div id="blog-topics" class="grid md:grid-cols-2 gap-4"></div>
213
- </div>
214
- <div id="blog-preview" class="hidden"></div>
215
- </div>
216
-
217
- <!-- Tab: Photo -->
218
- <div id="panel-photo" class="tab-panel hidden">
219
- <div class="card p-8">
220
- <h2 class="text-2xl font-bold mb-4">📸 Organism Photo Rotation</h2>
221
- <p class="text-gray-400 mb-6">Your photos rotate based on time of day, day of week, and engagement data.
222
- The profile breathes like a living organism.</p>
223
- <div id="photo-strategy" class="space-y-4"></div>
224
- </div>
225
- </div>
226
-
227
- <!-- Tab: Gravity -->
228
- <div id="panel-gravity" class="tab-panel hidden">
229
- <div class="card p-8">
230
- <h2 class="text-2xl font-bold mb-4">🌍 Availability Gravity Engine</h2>
231
- <p class="text-gray-400 mb-6">Dynamic availability windows that create urgency and drive repeat visits.
232
- RL-optimized for maximum booking conversion.</p>
233
- <div id="gravity-tactics" class="space-y-4"></div>
234
- </div>
235
- </div>
236
-
237
- <!-- Tab: Engine -->
238
- <div id="panel-engine" class="tab-panel hidden">
239
- <div class="card p-8">
240
- <h2 class="text-2xl font-bold mb-4">⚙️ Optimization Engine</h2>
241
- <p class="text-gray-400 mb-6">The full GA + RL + LLM pipeline running 24/7.</p>
242
- <div id="engine-components" class="space-y-3"></div>
243
- </div>
244
- </div>
245
-
246
- <!-- Tab: Search -->
247
- <div id="panel-search" class="tab-panel hidden">
248
- <div class="card p-8 mb-6">
249
- <h2 class="text-2xl font-bold mb-4">🔎 Browse the Market</h2>
250
- <p class="text-gray-400 mb-6">Search and rank all 2,738 masseurs by views/day, visits, reviews, and
251
- more.</p>
252
- <div class="flex flex-wrap gap-3 mb-4">
253
- <input id="search-city" type="text"
254
- placeholder="Username or city (e.g. karpathianwolf or manhattan-ny)"
255
- class="px-4 py-2 rounded-xl bg-white/5 border border-white/10 focus:border-violet-500 outline-none text-sm">
256
- <select id="search-sort"
257
- class="px-4 py-2 rounded-xl bg-white/5 border border-white/10 focus:border-violet-500 outline-none text-sm">
258
- <option value="views_per_day">Views/Day</option>
259
- <option value="visits">Total Visits</option>
260
- <option value="reviews_count">Reviews</option>
261
- <option value="rating">Rating</option>
262
- </select>
263
- <button onclick="searchMasseurs()"
264
- class="px-6 py-2 rounded-xl bg-gradient-to-r from-violet-500 to-fuchsia-500 font-semibold text-sm hover:opacity-90 transition">Search</button>
265
- </div>
266
- </div>
267
- <div id="search-results"></div>
268
- </div>
269
- </div>
270
-
271
- <script>
272
- // ─── Tab switching ─────────────────────────────────
273
- function switchTab(name) {
274
- document.querySelectorAll('.tab-panel').forEach(p => p.classList.add('hidden'));
275
- document.getElementById('panel-' + name).classList.remove('hidden');
276
- document.querySelectorAll('[id^="tab-"]').forEach(t => {
277
- t.classList.remove('tab-active');
278
- t.classList.add('bg-white/5', 'border', 'border-white/10');
279
- });
280
- const tab = document.getElementById('tab-' + name);
281
- tab.classList.add('tab-active');
282
- tab.classList.remove('bg-white/5', 'border', 'border-white/10');
283
- }
284
-
285
- // ─── Load market stats ─────────────────────────────
286
- fetch('/api/market-stats').then(r => r.json()).then(d => {
287
- document.getElementById('stat-total').textContent = d.total_masseurs.toLocaleString();
288
- document.getElementById('stat-cities').textContent = d.cities_covered;
289
- document.getElementById('stat-avgvpd').textContent = d.avg_views_per_day;
290
- document.getElementById('stat-gold').textContent = d.gold_members.toLocaleString();
291
- });
292
-
293
- // ─── Analyze profile ───────────────────────────────
294
- function analyzeProfile() {
295
- const username = document.getElementById('analyze-username').value.trim();
296
- if (!username) return;
297
- const results = document.getElementById('analyze-results');
298
- results.classList.remove('hidden');
299
- results.innerHTML = '<div class="card p-8 text-center"><div class="spin w-8 h-8 border-4 border-violet-500 border-t-transparent rounded-full mx-auto mb-4"></div>Analyzing ' + username + '...</div>';
300
- fetch('/api/analyze/' + username).then(r => r.json()).then(d => {
301
- if (d.error) {
302
- results.innerHTML = '<div class="card p-8 text-center text-gray-400">' + d.error + '</div>';
303
- return;
304
- }
305
- const percentile = d.percentile;
306
- const recs = d.recommendations.map(r => `<li class="flex items-start gap-3 text-gray-300"><span class="text-violet-400 mt-1">→</span> ${r}</li>`).join('');
307
- results.innerHTML = `
308
- <div class="grid md:grid-cols-2 gap-6 mb-6">
309
- <div class="card p-8">
310
- <div class="text-sm text-gray-500 mb-2">Your Rank</div>
311
- <div class="text-5xl font-black text-violet-400">#${d.rank}</div>
312
- <div class="text-gray-500 mt-2">out of ${d.total} masseurs worldwide</div>
313
- <div class="mt-4">
314
- <div class="flex justify-between text-sm mb-1"><span class="text-gray-500">Percentile</span><span class="text-violet-400 font-bold">${percentile}%</span></div>
315
- <div class="h-3 rounded-full bg-white/5 overflow-hidden"><div class="h-full progress-bar" style="width:${percentile}%"></div></div>
316
- </div>
317
- </div>
318
- <div class="card p-8">
319
- <div class="grid grid-cols-2 gap-4">
320
- <div><div class="text-sm text-gray-500">Views/Day</div><div class="text-2xl font-bold text-fuchsia-400">${d.views_per_day}</div></div>
321
- <div><div class="text-sm text-gray-500">Total Visits</div><div class="text-2xl font-bold text-pink-400">${d.total_visits.toLocaleString()}</div></div>
322
- <div><div class="text-sm text-gray-500">Member Since</div><div class="text-lg font-bold">${d.member_since}</div></div>
323
- <div><div class="text-sm text-gray-500">Reviews</div><div class="text-2xl font-bold text-amber-400">${d.reviews_count}</div></div>
324
- <div><div class="text-sm text-gray-500">Rating</div><div class="text-2xl font-bold text-green-400">${d.rating}★</div></div>
325
- <div><div class="text-sm text-gray-500">Photos</div><div class="text-2xl font-bold">${d.photo_count}</div></div>
326
- <div><div class="text-sm text-gray-500">Bio Length</div><div class="text-lg font-bold ${d.bio_length > d.top_avg_bio_length + 200 ? 'text-red-400' : 'text-green-400'}">${d.bio_length} chars</div></div>
327
- <div><div class="text-sm text-gray-500">Gold</div><div class="text-lg font-bold ${d.is_gold ? 'text-amber-400' : 'text-gray-600'}">${d.is_gold ? 'Yes' : 'No'}</div></div>
328
- </div>
329
- </div>
330
- </div>
331
- <div class="card p-8 mb-6">
332
- <h3 class="text-xl font-bold mb-4">Services</h3>
333
- <div class="flex flex-wrap gap-2">
334
- ${d.services.map(s => `<span class="px-4 py-2 rounded-full bg-violet-500/10 text-violet-300 text-sm">${s}</span>`).join('')}
335
- </div>
336
- </div>
337
- <div class="card p-8">
338
- <h3 class="text-xl font-bold mb-4">AI Recommendations</h3>
339
- ${recs ? `<ul class="space-y-3">${recs}</ul>` : '<p class="text-green-400">Your profile is well-optimized! No critical issues found.</p>'}
340
- <div class="mt-6 pt-6 border-t border-white/10">
341
- <div class="text-sm text-gray-500 mb-2">Top performer benchmark:</div>
342
- <div class="flex gap-6 text-sm">
343
- <span>Avg bio length: <strong class="text-violet-400">${d.top_avg_bio_length} chars</strong></span>
344
- <span>Avg reviews: <strong class="text-violet-400">${d.top_avg_reviews}</strong></span>
345
- </div>
346
- </div>
347
- </div>
348
- `;
349
- });
350
- }
351
-
352
- // ─── Generate bio ──────────────────────────────────
353
- function generateBio() {
354
- const username = document.getElementById('bio-username').value.trim();
355
- const style = document.getElementById('bio-style').value;
356
- if (!username) return;
357
- const results = document.getElementById('bio-results');
358
- results.classList.remove('hidden');
359
- results.innerHTML = '<div class="card p-8 text-center"><div class="spin w-8 h-8 border-4 border-violet-500 border-t-transparent rounded-full mx-auto mb-4"></div>Generating 30 variants...</div>';
360
- fetch('/api/generate-bio', {
361
- method: 'POST',
362
- headers: { 'Content-Type': 'application/json' },
363
- body: JSON.stringify({ username, style })
364
- }).then(r => r.json()).then(d => {
365
- results.innerHTML = `
366
- <div class="card p-8 mb-6">
367
- <h3 class="text-xl font-bold mb-2">Generated Bio — ${style} style</h3>
368
- <div class="text-xs text-gray-500 mb-4">Analyzed ${d.competitor_patterns_analyzed} top competitor patterns</div>
369
- <div class="bg-white/5 rounded-xl p-6 border border-white/10">
370
- <p class="text-gray-300 leading-relaxed">${d.preview}</p>
371
- </div>
372
- <div class="mt-6 flex gap-3">
373
- <button class="px-6 py-2 rounded-xl bg-gradient-to-r from-violet-500 to-fuchsia-500 font-semibold text-sm hover:opacity-90 transition">Push to Profile</button>
374
- <button onclick="generateBio()" class="px-6 py-2 rounded-xl border border-white/20 font-semibold text-sm hover:bg-white/5 transition">Regenerate</button>
375
- </div>
376
- </div>
377
- <div class="card p-8">
378
- <h3 class="text-lg font-bold mb-4">Top Competitor Patterns Detected</h3>
379
- <div class="space-y-3">
380
- ${d.top_patterns.map(p => `<div class="flex items-start gap-3"><span class="text-violet-400 text-sm font-bold shrink-0">@${p.username}</span><span class="text-gray-400 text-sm">${p.bio_preview}...</span><span class="text-xs text-gray-600 shrink-0">${p.vpd}/day</span></div>`).join('')}
381
- </div>
382
- </div>
383
- `;
384
- });
385
- }
386
-
387
- // ─── Blog topics ───────────────────────────────────
388
- fetch('/api/blog/topics').then(r => r.json()).then(d => {
389
- const container = document.getElementById('blog-topics');
390
- d.topics.forEach(t => {
391
- container.innerHTML += `
392
- <div class="card card-hover p-6 cursor-pointer" onclick="this.querySelector('.blog-angle').classList.toggle('hidden')">
393
- <h4 class="font-bold mb-1">${t.title}</h4>
394
- <p class="text-sm text-gray-500 blog-angle hidden mt-2">${t.angle}</p>
395
- <div class="mt-3 text-xs text-violet-400">Click to expand →</div>
396
- </div>
397
- `;
398
- });
399
- });
400
-
401
- // ─── Photo strategy ────────────────────────────────
402
- fetch('/api/photo-strategy').then(r => r.json()).then(d => {
403
- const container = document.getElementById('photo-strategy');
404
- container.innerHTML = `
405
- <div class="mb-6 p-4 rounded-xl bg-violet-500/10 border border-violet-500/20">
406
- <p class="text-gray-300">${d.description}</p>
407
- <p class="text-sm text-violet-300 mt-2">Rotation: ${d.rotation_frequency} · AI: ${d.ai_optimization}</p>
408
- </div>
409
- ${d.schedule.map(s => `
410
- <div class="flex items-center gap-4 p-4 rounded-xl bg-white/5 border border-white/10">
411
- <div class="w-12 h-12 rounded-xl bg-gradient-to-br from-violet-500/20 to-fuchsia-500/20 flex items-center justify-center text-xl shrink-0">📸</div>
412
- <div class="flex-1">
413
- <div class="font-semibold">${s.time}</div>
414
- <div class="text-sm text-gray-400">${s.type}</div>
415
- <div class="text-xs text-gray-600 mt-1">${s.reason}</div>
416
- </div>
417
- </div>
418
- `).join('')}
419
- `;
420
- });
421
-
422
- // ─── Availability gravity ──────────────────────────
423
- fetch('/api/availability-gravity').then(r => r.json()).then(d => {
424
- const container = document.getElementById('gravity-tactics');
425
- container.innerHTML = `
426
- <div class="mb-6 p-4 rounded-xl bg-cyan-500/10 border border-cyan-500/20">
427
- <p class="text-gray-300">${d.description}</p>
428
- </div>
429
- ${d.tactics.map(t => `
430
- <div class="p-5 rounded-xl bg-white/5 border border-white/10">
431
- <div class="flex items-center justify-between mb-2">
432
- <h4 class="font-bold text-lg">${t.name}</h4>
433
- <span class="px-3 py-1 rounded-full bg-cyan-500/10 text-cyan-300 text-xs font-bold">${t.impact}</span>
434
- </div>
435
- <p class="text-gray-400 text-sm">${t.description}</p>
436
- </div>
437
- `).join('')}
438
- <div class="mt-4 p-4 rounded-xl bg-gradient-to-r from-violet-500/10 to-fuchsia-500/10 border border-violet-500/20">
439
- <p class="text-sm text-gray-300">🧠 <strong>RL Optimization:</strong> ${d.rl_optimization}</p>
440
- </div>
441
- `;
442
- });
443
-
444
- // ─── Engine status ─────────────────────────────────
445
- fetch('/api/optimization-status').then(r => r.json()).then(d => {
446
- const container = document.getElementById('engine-components');
447
- container.innerHTML = `
448
- <div class="mb-4 p-4 rounded-xl bg-gradient-to-r from-violet-500/10 to-fuchsia-500/10 border border-violet-500/20">
449
- <p class="text-gray-300"><strong>Engine:</strong> ${d.engine}</p>
450
- <p class="text-sm text-gray-500 mt-1">${d.cycle}</p>
451
- <p class="text-sm text-violet-300 mt-1">📊 ${d.data_source}</p>
452
- </div>
453
- ${d.components.map(c => `
454
- <div class="flex items-center gap-4 p-4 rounded-xl bg-white/5 border border-white/10">
455
- <div class="w-3 h-3 rounded-full bg-green-400 pulse-dot shrink-0"></div>
456
- <div class="flex-1">
457
- <div class="font-semibold">${c.name}</div>
458
- <div class="text-sm text-gray-400">${c.description}</div>
459
- </div>
460
- <span class="text-xs text-green-400 font-bold uppercase">${c.status}</span>
461
- </div>
462
- `).join('')}
463
- `;
464
- });
465
-
466
- // ─── Search ────────────────────────────────────────
467
- function searchMasseurs() {
468
- const city = document.getElementById('search-city').value.trim();
469
- const sort = document.getElementById('search-sort').value;
470
- const params = new URLSearchParams({ sort, limit: 50 });
471
- if (city) params.set('q', city);
472
- const container = document.getElementById('search-results');
473
- container.innerHTML = '<div class="card p-8 text-center"><div class="spin w-8 h-8 border-4 border-violet-500 border-t-transparent rounded-full mx-auto"></div></div>';
474
- fetch('/api/search?' + params).then(r => r.json()).then(d => {
475
- container.innerHTML = `
476
- <div class="text-sm text-gray-500 mb-4">${d.total} results</div>
477
- <div class="card overflow-hidden">
478
- <table class="w-full text-sm">
479
- <thead class="bg-white/5 text-gray-400">
480
- <tr>
481
- <th class="text-left p-3">#</th>
482
- <th class="text-left p-3">Username</th>
483
- <th class="text-left p-3">City</th>
484
- <th class="text-right p-3">Views/Day</th>
485
- <th class="text-right p-3">Visits</th>
486
- <th class="text-right p-3">Reviews</th>
487
- <th class="text-center p-3">Gold</th>
488
- </tr>
489
- </thead>
490
- <tbody>
491
- ${d.results.map((r, i) => `
492
- <tr class="border-t border-white/5 hover:bg-white/5">
493
- <td class="p-3 text-gray-500">${i + 1}</td>
494
- <td class="p-3 font-semibold text-violet-400">
495
- <a href="/profile/${r.username}" class="hover:underline">@${r.username}</a>
496
- <a href="${r.profile_url}" target="_blank" class="ml-2 text-xs text-fuchsia-400 hover:text-fuchsia-300">↗ rentmasseur.com/${r.username}</a>
497
- </td>
498
- <td class="p-3 text-gray-400">${r.city}</td>
499
- <td class="p-3 text-right font-bold text-fuchsia-400">${r.vpd}</td>
500
- <td class="p-3 text-right text-gray-400">${r.visits.toLocaleString()}</td>
501
- <td class="p-3 text-right text-gray-400">${r.reviews}</td>
502
- <td class="p-3 text-center">${r.gold ? '⭐' : '—'}</td>
503
- </tr>
504
- `).join('')}
505
- </tbody>
506
- </table>
507
- </div>
508
- `;
509
- });
510
- }
511
-
512
- // Auto-load search on tab open
513
- searchMasseurs();
514
- </script>
515
- </body>
516
-
517
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
templates/landing.html DELETED
@@ -1,272 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>MasseurBoost — AI-Powered Profile Optimization for RentMasseur</title>
7
- <script src="https://cdn.tailwindcss.com"></script>
8
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
9
- <style>
10
- * { font-family: 'Inter', sans-serif; }
11
- .gradient-bg { background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%); }
12
- .glow { box-shadow: 0 0 40px rgba(139, 92, 246, 0.3); }
13
- .animate-float { animation: float 6s ease-in-out infinite; }
14
- @keyframes float { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-20px); } }
15
- .card-hover { transition: all 0.3s ease; }
16
- .card-hover:hover { transform: translateY(-8px); box-shadow: 0 20px 60px rgba(139,92,246,0.2); }
17
- .pulse-dot { animation: pulse-dot 2s infinite; }
18
- @keyframes pulse-dot { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
19
- </style>
20
- </head>
21
- <body class="bg-[#0a0a0f] text-white">
22
-
23
- <!-- Nav -->
24
- <nav class="fixed top-0 w-full z-50 bg-[#0a0a0f]/80 backdrop-blur-lg border-b border-white/5">
25
- <div class="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
26
- <div class="flex items-center gap-2">
27
- <div class="w-10 h-10 rounded-xl bg-gradient-to-br from-violet-500 to-fuchsia-500 flex items-center justify-center text-xl font-black">M</div>
28
- <span class="text-xl font-bold">Masseur<span class="text-violet-400">Boost</span></span>
29
- </div>
30
- <div class="hidden md:flex items-center gap-8 text-sm text-gray-400">
31
- <a href="#features" class="hover:text-white transition">Features</a>
32
- <a href="#how" class="hover:text-white transition">How It Works</a>
33
- <a href="#pricing" class="hover:text-white transition">Pricing</a>
34
- <a href="/dashboard" class="hover:text-white transition">Dashboard</a>
35
- </div>
36
- <a href="/dashboard" class="px-5 py-2 rounded-full bg-gradient-to-r from-violet-500 to-fuchsia-500 text-sm font-semibold hover:opacity-90 transition">Get Started</a>
37
- </div>
38
- </nav>
39
-
40
- <!-- Hero -->
41
- <section class="gradient-bg pt-40 pb-32 relative overflow-hidden">
42
- <div class="absolute inset-0 opacity-20" style="background-image: url('data:image/svg+xml,%3Csvg width=&quot;60&quot; height=&quot;60&quot; viewBox=&quot;0 0 60 60&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;%3E%3Cg fill=&quot;none&quot; fill-rule=&quot;evenodd&quot;%3E%3Cg fill=&quot;%238b5cf6&quot; fill-opacity=&quot;0.1&quot;%3E%3Ccircle cx=&quot;30&quot; cy=&quot;30&quot; r=&quot;2&quot;/%3E%3C/g%3E%3C/g%3E%3C/svg%3E')"></div>
43
- <div class="max-w-5xl mx-auto px-6 text-center relative z-10">
44
- <div class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-violet-500/10 border border-violet-500/30 mb-8">
45
- <span class="w-2 h-2 rounded-full bg-green-400 pulse-dot"></span>
46
- <span class="text-sm text-violet-300">Now analyzing 2,738 masseurs across 252 cities</span>
47
- </div>
48
- <h1 class="text-5xl md:text-7xl font-black leading-tight mb-6">
49
- Your profile isn't static.<br>
50
- <span class="bg-gradient-to-r from-violet-400 via-fuchsia-400 to-pink-400 bg-clip-text text-transparent">It's a living organism.</span>
51
- </h1>
52
- <p class="text-xl text-gray-400 max-w-2xl mx-auto mb-10">
53
- AI-powered bio optimization, blog generation, photo rotation, and availability gravity —
54
- all working 24/7 to make your RentMasseur profile attract more clients, more views, more bookings.
55
- </p>
56
- <div class="flex items-center justify-center gap-4">
57
- <a href="/dashboard" class="px-8 py-4 rounded-full bg-gradient-to-r from-violet-500 to-fuchsia-500 font-semibold text-lg hover:opacity-90 transition glow">Launch Dashboard</a>
58
- <a href="#features" class="px-8 py-4 rounded-full border border-white/20 font-semibold text-lg hover:bg-white/5 transition">See Features</a>
59
- </div>
60
- <div class="mt-16 flex items-center justify-center gap-12 text-center">
61
- <div>
62
- <div class="text-4xl font-black text-violet-400">2,738</div>
63
- <div class="text-sm text-gray-500">Profiles analyzed</div>
64
- </div>
65
- <div class="w-px h-12 bg-white/10"></div>
66
- <div>
67
- <div class="text-4xl font-black text-fuchsia-400">252</div>
68
- <div class="text-sm text-gray-500">Cities worldwide</div>
69
- </div>
70
- <div class="w-px h-12 bg-white/10"></div>
71
- <div>
72
- <div class="text-4xl font-black text-pink-400">3h</div>
73
- <div class="text-sm text-gray-500">Optimization cycle</div>
74
- </div>
75
- </div>
76
- </div>
77
- </section>
78
-
79
- <!-- Features -->
80
- <section id="features" class="py-32 bg-[#0a0a0f]">
81
- <div class="max-w-7xl mx-auto px-6">
82
- <div class="text-center mb-20">
83
- <h2 class="text-4xl md:text-5xl font-black mb-4">Everything your profile needs to win</h2>
84
- <p class="text-xl text-gray-500">Six AI engines working in parallel, 24/7</p>
85
- </div>
86
- <div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
87
-
88
- <!-- Bio Optimizer -->
89
- <div class="card-hover bg-gradient-to-br from-violet-500/10 to-transparent border border-violet-500/20 rounded-3xl p-8">
90
- <div class="w-14 h-14 rounded-2xl bg-violet-500/20 flex items-center justify-center mb-6 text-2xl">🧬</div>
91
- <h3 class="text-2xl font-bold mb-3">GA/RL Bio Optimizer</h3>
92
- <p class="text-gray-400 mb-4">Genetic Algorithm evolves your bio through A/B testing. Reinforcement Learning tracks which versions drive bookings. LLM generates mutations from top-performing competitor patterns.</p>
93
- <div class="flex flex-wrap gap-2">
94
- <span class="px-3 py-1 rounded-full bg-violet-500/10 text-violet-300 text-xs">30 variants tested</span>
95
- <span class="px-3 py-1 rounded-full bg-violet-500/10 text-violet-300 text-xs">Head-to-head scoring</span>
96
- <span class="px-3 py-1 rounded-full bg-violet-500/10 text-violet-300 text-xs">Auto-push winner</span>
97
- </div>
98
- </div>
99
-
100
- <!-- Blog Generator -->
101
- <div class="card-hover bg-gradient-to-br from-fuchsia-500/10 to-transparent border border-fuchsia-500/20 rounded-3xl p-8">
102
- <div class="w-14 h-14 rounded-2xl bg-fuchsia-500/20 flex items-center justify-center mb-6 text-2xl">✍️</div>
103
- <h3 class="text-2xl font-bold mb-3">LLM Blog Generator</h3>
104
- <p class="text-gray-400 mb-4">SEO-optimized blog posts generated from winning competitor patterns. Funny, engaging, and engineered to drive phone calls and bookings. Twice daily, automatically.</p>
105
- <div class="flex flex-wrap gap-2">
106
- <span class="px-3 py-1 rounded-full bg-fuchsia-500/10 text-fuchsia-300 text-xs">SEO keywords</span>
107
- <span class="px-3 py-1 rounded-full bg-fuchsia-500/10 text-fuchsia-300 text-xs">Booking CTAs</span>
108
- <span class="px-3 py-1 rounded-full bg-fuchsia-500/10 text-fuchsia-300 text-xs">Pattern-matched</span>
109
- </div>
110
- </div>
111
-
112
- <!-- Photo Rotation -->
113
- <div class="card-hover bg-gradient-to-br from-pink-500/10 to-transparent border border-pink-500/20 rounded-3xl p-8">
114
- <div class="w-14 h-14 rounded-2xl bg-pink-500/20 flex items-center justify-center mb-6 text-2xl">📸</div>
115
- <h3 class="text-2xl font-bold mb-3">Organism Photo Swap</h3>
116
- <p class="text-gray-400 mb-4">Your photos rotate based on time of day, day of week, and engagement data. Morning browsers see professional shots. Evening visitors see lifestyle. Your profile breathes.</p>
117
- <div class="flex flex-wrap gap-2">
118
- <span class="px-3 py-1 rounded-full bg-pink-500/10 text-pink-300 text-xs">Time-of-day AI</span>
119
- <span class="px-3 py-1 rounded-full bg-pink-500/10 text-pink-300 text-xs">GA-optimized order</span>
120
- <span class="px-3 py-1 rounded-full bg-pink-500/10 text-pink-300 text-xs">6h rotation</span>
121
- </div>
122
- </div>
123
-
124
- <!-- Availability Gravity -->
125
- <div class="card-hover bg-gradient-to-br from-cyan-500/10 to-transparent border border-cyan-500/20 rounded-3xl p-8">
126
- <div class="w-14 h-14 rounded-2xl bg-cyan-500/20 flex items-center justify-center mb-6 text-2xl">🌍</div>
127
- <h3 class="text-2xl font-bold mb-3">Availability Gravity</h3>
128
- <p class="text-gray-400 mb-4">Dynamic availability windows create urgency and drive repeat visits. "3 slots left this week" converts 40-60% better. RL optimizes which slots to show and when.</p>
129
- <div class="flex flex-wrap gap-2">
130
- <span class="px-3 py-1 rounded-full bg-cyan-500/10 text-cyan-300 text-xs">Scarcity engine</span>
131
- <span class="px-3 py-1 rounded-full bg-cyan-500/10 text-cyan-300 text-xs">Peak hour alignment</span>
132
- <span class="px-3 py-1 rounded-full bg-cyan-500/10 text-cyan-300 text-xs">Revisit triggers</span>
133
- </div>
134
- </div>
135
-
136
- <!-- Interview Rotator -->
137
- <div class="card-hover bg-gradient-to-br from-amber-500/10 to-transparent border border-amber-500/20 rounded-3xl p-8">
138
- <div class="w-14 h-14 rounded-2xl bg-amber-500/20 flex items-center justify-center mb-6 text-2xl">🎤</div>
139
- <h3 class="text-2xl font-bold mb-3">Interview Q&A Engine</h3>
140
- <p class="text-gray-400 mb-4">Funny, engaging interview Q&A that make clients want to call. LLM generates fresh angles daily — from origin stories to myth-busting to guilty pleasures. Each answer ends with a booking CTA.</p>
141
- <div class="flex flex-wrap gap-2">
142
- <span class="px-3 py-1 rounded-full bg-amber-500/10 text-amber-300 text-xs">20+ angles</span>
143
- <span class="px-3 py-1 rounded-full bg-amber-500/10 text-amber-300 text-xs">Humor-driven</span>
144
- <span class="px-3 py-1 rounded-full bg-amber-500/10 text-amber-300 text-xs">Daily rotation</span>
145
- </div>
146
- </div>
147
-
148
- <!-- Price Optimizer -->
149
- <div class="card-hover bg-gradient-to-br from-emerald-500/10 to-transparent border border-emerald-500/20 rounded-3xl p-8">
150
- <div class="w-14 h-14 rounded-2xl bg-emerald-500/20 flex items-center justify-center mb-6 text-2xl">💰</div>
151
- <h3 class="text-2xl font-bold mb-3">Price Optimization</h3>
152
- <p class="text-gray-400 mb-4">GA tests price points against your competitor set to maximize revenue, not just bookings. Find the sweet spot where demand meets profit. RL tracks conversion at each price level.</p>
153
- <div class="flex flex-wrap gap-2">
154
- <span class="px-3 py-1 rounded-full bg-emerald-500/10 text-emerald-300 text-xs">Revenue-focused</span>
155
- <span class="px-3 py-1 rounded-full bg-emerald-500/10 text-emerald-300 text-xs">Competitor-aware</span>
156
- <span class="px-3 py-1 rounded-full bg-emerald-500/10 text-emerald-300 text-xs">RL-tracked</span>
157
- </div>
158
- </div>
159
- </div>
160
- </div>
161
- </section>
162
-
163
- <!-- How It Works -->
164
- <section id="how" class="py-32 bg-gradient-to-b from-[#0a0a0f] to-[#111118]">
165
- <div class="max-w-5xl mx-auto px-6">
166
- <div class="text-center mb-20">
167
- <h2 class="text-4xl md:text-5xl font-black mb-4">How the engine works</h2>
168
- <p class="text-xl text-gray-500">A continuous loop — scrape, analyze, optimize, push, measure, repeat</p>
169
- </div>
170
- <div class="space-y-4">
171
- <div class="flex items-center gap-6 bg-white/5 rounded-2xl p-6 border border-white/10">
172
- <div class="w-12 h-12 rounded-full bg-violet-500 flex items-center justify-center font-bold text-lg shrink-0">1</div>
173
- <div>
174
- <h3 class="text-xl font-bold mb-1">Scrape all competitors</h3>
175
- <p class="text-gray-400">Search API pulls every masseur in every city. Profile HTML gives us visits, member since, bio, services, photos, reviews. 2,738 profiles and counting.</p>
176
- </div>
177
- </div>
178
- <div class="flex items-center gap-6 bg-white/5 rounded-2xl p-6 border border-white/10">
179
- <div class="w-12 h-12 rounded-full bg-fuchsia-500 flex items-center justify-center font-bold text-lg shrink-0">2</div>
180
- <div>
181
- <h3 class="text-xl font-bold mb-1">Rank by views per day</h3>
182
- <p class="text-gray-400">Compute views_per_day = total_visits / days_since_registration. Rank all 2,738 masseurs. Identify what top performers do differently.</p>
183
- </div>
184
- </div>
185
- <div class="flex items-center gap-6 bg-white/5 rounded-2xl p-6 border border-white/10">
186
- <div class="w-12 h-12 rounded-full bg-pink-500 flex items-center justify-center font-bold text-lg shrink-0">3</div>
187
- <div>
188
- <h3 class="text-xl font-bold mb-1">Generate 30 variants via LLM</h3>
189
- <p class="text-gray-400">Groq LLM generates 30 bio variants across 10 strategies, informed by winning patterns. Each scored on CTA strength, urgency, emotional hook, SEO, uniqueness.</p>
190
- </div>
191
- </div>
192
- <div class="flex items-center gap-6 bg-white/5 rounded-2xl p-6 border border-white/10">
193
- <div class="w-12 h-12 rounded-full bg-cyan-500 flex items-center justify-center font-bold text-lg shrink-0">4</div>
194
- <div>
195
- <h3 class="text-xl font-bold mb-1">A/B test head-to-head</h3>
196
- <p class="text-gray-400">Pairwise LLM comparison eliminates weak variants. Top 3 face off. Winner must beat current live bio to qualify for push.</p>
197
- </div>
198
- </div>
199
- <div class="flex items-center gap-6 bg-white/5 rounded-2xl p-6 border border-white/10">
200
- <div class="w-12 h-12 rounded-full bg-amber-500 flex items-center justify-center font-bold text-lg shrink-0">5</div>
201
- <div>
202
- <h3 class="text-xl font-bold mb-1">Push to live profile</h3>
203
- <p class="text-gray-400">Winning bio, blog post, interview set, and photo order pushed via RentMasseur API. Receipts written for audit trail.</p>
204
- </div>
205
- </div>
206
- <div class="flex items-center gap-6 bg-white/5 rounded-2xl p-6 border border-white/10">
207
- <div class="w-12 h-12 rounded-full bg-emerald-500 flex items-center justify-center font-bold text-lg shrink-0">6</div>
208
- <div>
209
- <h3 class="text-xl font-bold mb-1">RL feedback loop</h3>
210
- <p class="text-gray-400">Scrape dashboard stats: views, phone clicks, emails, bookings. Calculate reward. If engagement drops, trigger rotation. Repeat every 3 hours.</p>
211
- </div>
212
- </div>
213
- </div>
214
- </div>
215
- </section>
216
-
217
- <!-- Pricing -->
218
- <section id="pricing" class="py-32 bg-[#0a0a0f]">
219
- <div class="max-w-6xl mx-auto px-6">
220
- <div class="text-center mb-20">
221
- <h2 class="text-4xl md:text-5xl font-black mb-4">Pricing that scales with you</h2>
222
- <p class="text-xl text-gray-500">From first optimization to full autonomy</p>
223
- </div>
224
- <div class="grid md:grid-cols-3 gap-8" id="pricing-cards">
225
- <!-- Filled by JS -->
226
- </div>
227
- </div>
228
- </section>
229
-
230
- <!-- CTA -->
231
- <section class="py-32 gradient-bg">
232
- <div class="max-w-3xl mx-auto px-6 text-center">
233
- <h2 class="text-4xl md:text-5xl font-black mb-6">Your competitors are already optimizing.</h2>
234
- <p class="text-xl text-gray-400 mb-10">While you're reading this, top performers are running GA loops, testing bios, and rotating photos. Don't get left behind.</p>
235
- <a href="/dashboard" class="inline-block px-10 py-5 rounded-full bg-gradient-to-r from-violet-500 to-fuchsia-500 font-bold text-xl hover:opacity-90 transition glow">Launch Your Dashboard →</a>
236
- </div>
237
- </section>
238
-
239
- <!-- Footer -->
240
- <footer class="py-12 bg-[#0a0a0f] border-t border-white/5">
241
- <div class="max-w-7xl mx-auto px-6 flex flex-col md:flex-row items-center justify-between gap-4">
242
- <div class="flex items-center gap-2">
243
- <div class="w-8 h-8 rounded-lg bg-gradient-to-br from-violet-500 to-fuchsia-500 flex items-center justify-center font-black">M</div>
244
- <span class="font-bold">MasseurBoost</span>
245
- </div>
246
- <p class="text-sm text-gray-600">AI-powered profile optimization · 2,738 profiles · 252 cities · GA + RL + LLM</p>
247
- </div>
248
- </footer>
249
-
250
- <script>
251
- // Load pricing
252
- fetch('/api/pricing').then(r => r.json()).then(data => {
253
- const container = document.getElementById('pricing-cards');
254
- data.plans.forEach(plan => {
255
- const popular = plan.popular ? 'border-violet-500 glow' : 'border-white/10';
256
- const badge = plan.popular ? '<div class="absolute -top-3 left-1/2 -translate-x-1/2 px-4 py-1 rounded-full bg-gradient-to-r from-violet-500 to-fuchsia-500 text-xs font-bold">MOST POPULAR</div>' : '';
257
- container.innerHTML += `
258
- <div class="relative bg-white/5 ${popular} border rounded-3xl p-8 card-hover">
259
- ${badge}
260
- <h3 class="text-2xl font-bold mb-2">${plan.name}</h3>
261
- <div class="text-4xl font-black mb-6">${plan.price}</div>
262
- <ul class="space-y-3">
263
- ${plan.features.map(f => `<li class="flex items-start gap-3 text-gray-400"><span class="text-violet-400 mt-1">✓</span> ${f}</li>`).join('')}
264
- </ul>
265
- <a href="/dashboard" class="block mt-8 py-3 rounded-full text-center ${plan.popular ? 'bg-gradient-to-r from-violet-500 to-fuchsia-500' : 'border border-white/20 hover:bg-white/5'} font-semibold transition">Get Started</a>
266
- </div>
267
- `;
268
- });
269
- });
270
- </script>
271
- </body>
272
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
templates/profile.html DELETED
@@ -1,168 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>@{{ profile.username }} — MasseurBoost Profile</title>
7
- <script src="https://cdn.tailwindcss.com"></script>
8
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
9
- <style>
10
- * { font-family: 'Inter', sans-serif; }
11
- .gradient-bg { background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%); }
12
- .card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 1.5rem; }
13
- .progress-bar { background: linear-gradient(90deg, #8b5cf6, #d946ef, #ec4899); }
14
- .pulse-dot { animation: pulse-dot 2s infinite; }
15
- @keyframes pulse-dot { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
16
- </style>
17
- </head>
18
- <body class="bg-[#0a0a0f] text-white min-h-screen">
19
-
20
- <!-- Top bar -->
21
- <div class="fixed top-0 w-full z-50 bg-[#0a0a0f]/80 backdrop-blur-lg border-b border-white/5">
22
- <div class="max-w-5xl mx-auto px-6 py-3 flex items-center justify-between">
23
- <div class="flex items-center gap-3">
24
- <a href="/dashboard" class="flex items-center gap-2">
25
- <div class="w-9 h-9 rounded-xl bg-gradient-to-br from-violet-500 to-fuchsia-500 flex items-center justify-center font-black">M</div>
26
- <span class="font-bold">Masseur<span class="text-violet-400">Boost</span></span>
27
- </a>
28
- <span class="text-gray-600">/</span>
29
- <span class="text-gray-400 text-sm">Profile</span>
30
- </div>
31
- <a href="{{ rentmasseur_url }}" target="_blank" class="text-sm text-violet-400 hover:text-violet-300 transition">View on RentMasseur ↗</a>
32
- </div>
33
- </div>
34
-
35
- {% if error %}
36
- <div class="pt-24 max-w-2xl mx-auto px-6 text-center">
37
- <h1 class="text-3xl font-black mb-4">Profile not found</h1>
38
- <p class="text-gray-500">@{{ username }} is not in the database (3,374+ profiles scraped).</p>
39
- <a href="/dashboard" class="inline-block mt-6 px-6 py-3 rounded-xl bg-gradient-to-r from-violet-500 to-fuchsia-500 font-semibold">← Back to Dashboard</a>
40
- </div>
41
- {% else %}
42
-
43
- <div class="pt-20 max-w-5xl mx-auto px-6 py-8">
44
-
45
- <!-- Profile header -->
46
- <div class="card p-8 mb-6">
47
- <div class="flex items-start justify-between flex-wrap gap-4">
48
- <div>
49
- <div class="flex items-center gap-3 mb-2">
50
- <h1 class="text-3xl font-black">@{{ profile.username }}</h1>
51
- {% if profile.is_gold %}<span class="px-3 py-1 rounded-full bg-amber-500/10 text-amber-400 text-xs font-bold">⭐ GOLD</span>{% endif %}
52
- {% if profile.is_online %}<span class="px-3 py-1 rounded-full bg-green-500/10 text-green-400 text-xs font-bold pulse-dot">● ONLINE</span>{% endif %}
53
- </div>
54
- <p class="text-gray-400">{{ profile.location or profile.city }} · Member since {{ profile.member_since or '?' }}</p>
55
- {% if profile.headline %}<p class="text-violet-300 mt-2 text-lg">{{ profile.headline }}</p>{% endif %}
56
- </div>
57
- <div class="text-right">
58
- <div class="text-sm text-gray-500">Global Rank</div>
59
- <div class="text-4xl font-black text-violet-400">#{{ rank }}</div>
60
- <div class="text-xs text-gray-600">of {{ total }} masseurs</div>
61
- </div>
62
- </div>
63
- </div>
64
-
65
- <!-- Stats grid -->
66
- <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
67
- <div class="card p-6">
68
- <div class="text-sm text-gray-500 mb-1">Views/Day</div>
69
- <div class="text-3xl font-black text-fuchsia-400">{{ profile.views_per_day }}</div>
70
- </div>
71
- <div class="card p-6">
72
- <div class="text-sm text-gray-500 mb-1">Total Visits</div>
73
- <div class="text-3xl font-black text-pink-400">{{ "{:,}".format(profile.visits) }}</div>
74
- </div>
75
- <div class="card p-6">
76
- <div class="text-sm text-gray-500 mb-1">Rating</div>
77
- <div class="text-3xl font-black text-green-400">{{ profile.rating }}★</div>
78
- <div class="text-xs text-gray-600 mt-1">{{ profile.reviews_count }} reviews</div>
79
- </div>
80
- <div class="card p-6">
81
- <div class="text-sm text-gray-500 mb-1">Experience</div>
82
- <div class="text-3xl font-black text-amber-400">{{ profile.experience_years }}y</div>
83
- </div>
84
- </div>
85
-
86
- <!-- Percentile bar -->
87
- <div class="card p-6 mb-6">
88
- <div class="flex justify-between text-sm mb-2">
89
- <span class="text-gray-500">Percentile Ranking</span>
90
- <span class="text-violet-400 font-bold">Top {{ (100 - percentile) }}% ({{ percentile }}th percentile)</span>
91
- </div>
92
- <div class="h-4 rounded-full bg-white/5 overflow-hidden">
93
- <div class="h-full progress-bar" style="width:{{ percentile }}%"></div>
94
- </div>
95
- </div>
96
-
97
- <!-- Bio -->
98
- {% if profile.bio %}
99
- <div class="card p-8 mb-6">
100
- <h2 class="text-xl font-bold mb-4">Bio</h2>
101
- <div class="bg-white/5 rounded-xl p-6 border border-white/10">
102
- <p class="text-gray-300 leading-relaxed whitespace-pre-wrap">{{ profile.bio[:3000] }}</p>
103
- </div>
104
- <div class="text-xs text-gray-600 mt-3">Bio length: {{ profile.bio|length }} chars · Content hash: {{ profile.thumbnail or 'n/a' }}</div>
105
- </div>
106
- {% endif %}
107
-
108
- <!-- Services -->
109
- {% if services %}
110
- <div class="card p-8 mb-6">
111
- <h2 class="text-xl font-bold mb-4">Services</h2>
112
- <div class="flex flex-wrap gap-2">
113
- {% for s in services %}
114
- <span class="px-4 py-2 rounded-full bg-violet-500/10 text-violet-300 text-sm">{{ s }}</span>
115
- {% endfor %}
116
- </div>
117
- </div>
118
- {% endif %}
119
-
120
- <!-- Details grid -->
121
- <div class="card p-8 mb-6">
122
- <h2 class="text-xl font-bold mb-4">Profile Details</h2>
123
- <div class="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm">
124
- <div><span class="text-gray-500">Username:</span> <span class="font-semibold">@{{ profile.username }}</span></div>
125
- <div><span class="text-gray-500">User ID:</span> <span class="font-semibold">{{ profile.user_id or 'N/A' }}</span></div>
126
- <div><span class="text-gray-500">City:</span> <span class="font-semibold">{{ profile.city }}</span></div>
127
- <div><span class="text-gray-500">Location:</span> <span class="font-semibold">{{ profile.location or 'N/A' }}</span></div>
128
- <div><span class="text-gray-500">Member Since:</span> <span class="font-semibold">{{ profile.member_since or 'N/A' }}</span></div>
129
- <div><span class="text-gray-500">Last Login:</span> <span class="font-semibold">{{ profile.last_login or 'N/A' }}</span></div>
130
- <div><span class="text-gray-500">Photo Count:</span> <span class="font-semibold">{{ profile.photo_count }}</span></div>
131
- <div><span class="text-gray-500">Gold Member:</span> <span class="font-semibold">{{ 'Yes' if profile.is_gold else 'No' }}</span></div>
132
- <div><span class="text-gray-500">Online:</span> <span class="font-semibold">{{ 'Yes' if profile.is_online else 'No' }}</span></div>
133
- <div><span class="text-gray-500">Scraped At:</span> <span class="font-semibold">{{ profile.scraped_at or 'N/A' }}</span></div>
134
- </div>
135
- </div>
136
-
137
- <!-- City peers -->
138
- {% if city_peers %}
139
- <div class="card p-8 mb-6">
140
- <h2 class="text-xl font-bold mb-4">Top Peers in {{ profile.city }}</h2>
141
- <div class="space-y-2">
142
- {% for peer in city_peers %}
143
- <a href="/profile/{{ peer.username }}" class="flex items-center justify-between p-4 rounded-xl bg-white/5 border border-white/10 hover:bg-white/10 transition">
144
- <span class="font-semibold text-violet-400">@{{ peer.username }}</span>
145
- <div class="flex gap-6 text-sm">
146
- <span class="text-fuchsia-400 font-bold">{{ peer.views_per_day }}/day</span>
147
- <span class="text-gray-400">{{ "{:,}".format(peer.visits) }} visits</span>
148
- </div>
149
- </a>
150
- {% endfor %}
151
- </div>
152
- </div>
153
- {% endif %}
154
-
155
- <!-- Proof footer -->
156
- <div class="card p-6 mb-6 border-violet-500/20">
157
- <div class="flex items-center gap-3 text-sm text-gray-400">
158
- <span class="w-2 h-2 rounded-full bg-green-400 pulse-dot"></span>
159
- <span>Scraped from <a href="{{ rentmasseur_url }}" target="_blank" class="text-violet-400 hover:underline">rentmasseur.com/{{ profile.username }}</a> on {{ profile.scraped_at or 'unknown date' }}</span>
160
- </div>
161
- </div>
162
-
163
- <a href="/dashboard" class="inline-block mb-8 text-violet-400 hover:text-violet-300 transition">← Back to Dashboard</a>
164
- </div>
165
-
166
- {% endif %}
167
- </body>
168
- </html>