Files
html/api/live-screenshot.php

124 lines
6.0 KiB
PHP

<?php
/* LIVE SCREENSHOT — captures any page on demand via Playwright + chromium headless
Cache: 60s. Falls back to cached image if exists.
Usage: /api/live-screenshot.php?url=https://weval-consulting.com/ethica-hub.html
Or: /api/live-screenshot.php?path=/ethica-hub.html (auto-prepends domain)
*/
header("Cache-Control: max-age=600, public");
$cache_dir = "/dev/shm/live-screenshots";
@mkdir($cache_dir, 0755, true);
$url = $_GET["url"] ?? "";
$path = $_GET["path"] ?? "";
if ($path && !$url) {
$url = "https://weval-consulting.com" . (strpos($path, "/") === 0 ? $path : "/$path");
}
if (!$url) {
http_response_code(400);
echo "Need ?url= or ?path=";
exit;
}
// For API/PHP endpoints — render JSON pretty
$is_api = preg_match("/\/api\/.*\.php$/", $url);
// Cache key
$key = md5($url);
$cache = "$cache_dir/$key.png";
// Use cached if < 5min old
if (file_exists($cache) && (time() - filemtime($cache)) < 600) {
header("Content-Type: image/png");
header("X-Cache: HIT");
readfile($cache);
exit;
}
// OPUS FALLBACK — for S95 Arsenal/API URLs that systematically 500, generate status card instead of launching Playwright
$is_s95 = (preg_match('#/(w|wv)/(api|commonia|includes|production|widgets|artifacts|statistics|cli|docs)#', $url) || preg_match('#95\.216|:5890|:8443|s95#i', $url));
if ($is_s95 || $is_api) {
// Quick HTTP check first
$ch_test = curl_init($url);
curl_setopt_array($ch_test, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_NOBODY=>true, CURLOPT_FOLLOWLOCATION=>true, CURLOPT_SSL_VERIFYPEER=>false]);
curl_exec($ch_test);
$http_code = curl_getinfo($ch_test, CURLINFO_HTTP_CODE);
$total_time = round(curl_getinfo($ch_test, CURLINFO_TOTAL_TIME) * 1000);
curl_close($ch_test);
// If 200 OK and it's an API endpoint: render the JSON response as code
if ($http_code >= 200 && $http_code < 400 && $is_api) {
// Let Playwright handle it (will render the page)
} else if ($http_code >= 400 || $http_code === 0) {
// Generate SVG status card → PNG via Playwright minimal render
$short_url = preg_replace('#^https?://[^/]+#', '', $url);
$status_color = ($http_code >= 500) ? '#ef4444' : (($http_code >= 400) ? '#f59e0b' : '#6b7280');
$server_label = $is_s95 ? 'S95 Arsenal' : 'S204';
$svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 640"><rect fill="#0f172a" width="1024" height="640" rx="12"/>'
. '<rect fill="#1e293b" x="40" y="40" width="944" height="560" rx="8"/>'
. '<circle cx="512" cy="200" r="50" fill="' . $status_color . '" opacity="0.15"/>'
. '<text x="512" y="215" text-anchor="middle" fill="' . $status_color . '" font-family="monospace" font-size="42" font-weight="bold">' . $http_code . '</text>'
. '<text x="512" y="300" text-anchor="middle" fill="#94a3b8" font-family="sans-serif" font-size="16">' . htmlspecialchars($short_url) . '</text>'
. '<text x="512" y="340" text-anchor="middle" fill="#475569" font-family="monospace" font-size="13">' . $server_label . ' | ' . $total_time . 'ms</text>'
. '<text x="512" y="430" text-anchor="middle" fill="#334155" font-family="sans-serif" font-size="11">Screenshot unavailable — endpoint returned ' . $http_code . '</text>'
. '</svg>';
// OPUS Phase5 — Serve SVG DIRECTEMENT (zéro Playwright, zéro latence)
// Cache le SVG aussi pour éviter de régénérer
@file_put_contents("$cache_dir/$key.svg", $svg);
header("Content-Type: image/svg+xml");
header("X-Cache: SVG-DIRECT");
header("X-Status: " . $http_code);
echo $svg;
exit;
}
}
// Generate via Playwright Python
$py = "
import sys, asyncio
from playwright.async_api import async_playwright
async def cap():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True, args=[\"--no-sandbox\",\"--disable-dev-shm-usage\"])
ctx = await browser.new_context(viewport={\"width\":1024,\"height\":640}, ignore_https_errors=True)
page = await ctx.new_page()
try:
await page.goto(\"$url\", timeout=8000, wait_until=\"domcontentloaded\")
await page.wait_for_timeout(800)
await page.screenshot(path=\"$cache\", clip={\"x\":0,\"y\":0,\"width\":1024,\"height\":640})
except Exception as e:
# Fallback: render error message as image
await page.set_content(f\"<html><body style=font:18px monospace;background:#1a0a0a;color:#ff6b6b;padding:40px>SCREENSHOT FAIL<br><br>{type(e).__name__}<br>{str(e)[:200]}<br><br>URL: $url</body></html>\")
await page.screenshot(path=\"$cache\")
await browser.close()
asyncio.run(cap())
";
$tmp = tempnam("/tmp", "ss_");
file_put_contents($tmp, $py);
$out = shell_exec("timeout 12 python3 $tmp 2>&1");
@unlink($tmp);
if (file_exists($cache) && filesize($cache) > 100) {
header("Content-Type: image/png");
header("X-Cache: MISS");
header("X-Generator: playwright");
readfile($cache);
} else {
// OPUS Phase5 — Fallback SVG générique au lieu de 503 brut
$short = preg_replace("#^https?://[^/]+#", "", $url);
$fallback_svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 640"><rect fill="#0f172a" width="1024" height="640" rx="12"/>'
. '<rect fill="#1e293b" x="40" y="40" width="944" height="560" rx="8"/>'
. '<circle cx="512" cy="200" r="50" fill="#475569" opacity="0.2"/>'
. '<text x="512" y="210" text-anchor="middle" fill="#64748b" font-family="monospace" font-size="30">⏳</text>'
. '<text x="512" y="300" text-anchor="middle" fill="#94a3b8" font-family="sans-serif" font-size="14">' . htmlspecialchars($short) . '</text>'
. '<text x="512" y="340" text-anchor="middle" fill="#334155" font-family="monospace" font-size="11">Screenshot en cours de génération</text>'
. '</svg>';
header("Content-Type: image/svg+xml");
header("X-Cache: PLACEHOLDER");
echo $fallback_svg;
}