Wire admin studio SSR and search infrastructure

This commit is contained in:
2026-05-01 11:46:06 +02:00
parent 257b0dbef6
commit 18cea8b0f0
329 changed files with 197465 additions and 2741 deletions

160
scripts/check_inertia.php Normal file
View File

@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
function usage(): void
{
$script = basename(__FILE__);
fwrite(STDERR, <<<TXT
Usage:
php scripts/{$script} --base=https://skinbase.top /@gregor /categories /wallpapers
php scripts/{$script} https://skinbase.top/@gregor https://skinbase.top/categories
Classifications:
INERTIA_SSR id="app" + data-page + non-empty app HTML
INERTIA_NO_SSR id="app" + data-page + empty app HTML
NOT_INERTIA missing id="app" or missing data-page
TXT);
}
function isAbsoluteUrl(string $value): bool
{
return (bool) preg_match('~^https?://~i', $value);
}
function joinUrl(string $baseUrl, string $path): string
{
return rtrim($baseUrl, '/') . '/' . ltrim($path, '/');
}
function resolveTargets(array $arguments): array
{
$baseUrl = null;
$targets = [];
foreach ($arguments as $argument) {
if (str_starts_with($argument, '--base=')) {
$baseUrl = substr($argument, 7);
continue;
}
if ($argument === '--help' || $argument === '-h') {
usage();
exit(0);
}
$targets[] = $argument;
}
if ($targets === []) {
usage();
exit(1);
}
return array_map(static function (string $target) use ($baseUrl): string {
if (isAbsoluteUrl($target)) {
return $target;
}
if ($baseUrl === null || $baseUrl === '') {
fwrite(STDERR, "Relative path '{$target}' requires --base=...\n");
exit(1);
}
return joinUrl($baseUrl, $target);
}, $targets);
}
function fetchUrl(string $url): array
{
if (function_exists('curl_init')) {
$handle = curl_init($url);
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_TIMEOUT => 20,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_USERAGENT => 'Skinbase Inertia Checker/1.0',
CURLOPT_HEADER => true,
]);
$raw = curl_exec($handle);
if ($raw === false) {
$error = curl_error($handle);
curl_close($handle);
throw new RuntimeException($error !== '' ? $error : 'Unknown cURL error');
}
$status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
$headerSize = (int) curl_getinfo($handle, CURLINFO_HEADER_SIZE);
curl_close($handle);
return [
'status' => $status,
'body' => substr($raw, $headerSize),
];
}
$context = stream_context_create([
'http' => [
'method' => 'GET',
'follow_location' => 1,
'max_redirects' => 5,
'timeout' => 20,
'header' => "User-Agent: Skinbase Inertia Checker/1.0\r\n",
'ignore_errors' => true,
],
]);
$body = @file_get_contents($url, false, $context);
if ($body === false) {
$error = error_get_last();
throw new RuntimeException($error['message'] ?? 'HTTP request failed');
}
$headers = $http_response_header ?? [];
$status = 0;
foreach ($headers as $header) {
if (preg_match('~^HTTP/\S+\s+(\d{3})~', $header, $matches)) {
$status = (int) $matches[1];
}
}
return [
'status' => $status,
'body' => $body,
];
}
function classifyHtml(string $html): string
{
$hasApp = str_contains($html, 'id="app"');
$hasDataPage = str_contains($html, 'data-page="');
if (! $hasApp || ! $hasDataPage) {
return 'NOT_INERTIA';
}
if (! preg_match('~<div id="app"[^>]*>(.*?)</div>~si', $html, $matches)) {
return 'INERTIA_UNKNOWN';
}
return trim($matches[1]) === '' ? 'INERTIA_NO_SSR' : 'INERTIA_SSR';
}
$targets = resolveTargets(array_slice($argv, 1));
foreach ($targets as $url) {
try {
$response = fetchUrl($url);
$classification = classifyHtml($response['body']);
printf("%d\t%s\t%s\n", $response['status'], $classification, $url);
} catch (Throwable $exception) {
printf("ERROR\t%s\t%s\n", $exception->getMessage(), $url);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -44,7 +44,7 @@ DB::connection('legacy')
foreach ($rows as $r) {
$id = (int) ($r->user_id ?? 0);
$hash = trim((string) ($r->password2 ?: $r->password ?: ''));
if ($id === 0 || $hash === '') {
if ($id === 0 || $hash === '' || $hash === 'abc123') {
continue;
}

18
scripts/parse-a11y.cjs Normal file
View File

@@ -0,0 +1,18 @@
const fs = require('fs')
const html = fs.readFileSync('D:/Sites/Skinbase26/skinbase.top-20260429T075355.html', 'utf8')
const idx = html.indexOf('"requestedUrl"')
let start = idx; while(start>0 && html[start]!=='{') start--
let depth=0,end=0
const slice = html.slice(start)
for(let i=0;i<slice.length;i++){if(slice[i]==='{')depth++;else if(slice[i]==='}'){depth--;if(depth===0){end=i+1;break}}}
const lhr = JSON.parse(slice.slice(0,end))
const audits = ['link-name','color-contrast','label-content-name-mismatch','link-in-text-block','forced-reflow-insight','image-delivery-insight']
for (const id of audits) {
const a = lhr.audits[id]
if (!a) continue
console.log(`\n=== ${id} (score ${a.score}) ===`)
;(a.details?.items || []).forEach(item => {
if (item.type === 'node') console.log(' node:', item.snippet?.slice(0,200))
else console.log(' item:', JSON.stringify(item).slice(0,300))
})
}

10
scripts/parse-lcp.cjs Normal file
View File

@@ -0,0 +1,10 @@
const fs = require('fs')
const html = fs.readFileSync('D:/Sites/Skinbase26/skinbase.top-20260429T075355.html', 'utf8')
const idx = html.indexOf('"requestedUrl"')
let start = idx; while(start>0 && html[start]!=='{') start--
let depth=0,end=0
const slice = html.slice(start)
for(let i=0;i<slice.length;i++){if(slice[i]==='{')depth++;else if(slice[i]==='}'){depth--;if(depth===0){end=i+1;break}}}
const lhr = JSON.parse(slice.slice(0,end))
const lcp = lhr.audits['lcp-discovery-insight']
console.log(JSON.stringify(lcp, null, 2))

View File

@@ -0,0 +1,75 @@
const fs = require('fs')
const path = require('path')
const html = fs.readFileSync('D:/Sites/Skinbase26/skinbase.top-20260429T075355.html', 'utf8')
// The LH report embeds JSON like: const __json__ = ... or as a compressed string
// Try to find the JSON blob by locating "requestedUrl"
const idx = html.indexOf('"requestedUrl"')
if (idx === -1) {
console.log('No requestedUrl found in file')
process.exit(1)
}
// Walk backwards to find the opening { of the top-level object
let start = idx
while (start > 0 && html[start] !== '{') start--
// Walk forward to find the matching close — take a large slice and try to parse
const slice = html.slice(start)
// Find the end by counting braces
let depth = 0
let end = 0
for (let i = 0; i < slice.length; i++) {
if (slice[i] === '{') depth++
else if (slice[i] === '}') {
depth--
if (depth === 0) {
end = i + 1
break
}
}
}
let lhr
try {
lhr = JSON.parse(slice.slice(0, end))
} catch {
// Try taking the slice up to the first </script> after requestedUrl
const scriptEnd = html.indexOf('</script>', idx)
const scriptSlice = html.slice(start, scriptEnd).replace(/;?\s*$/, '')
lhr = JSON.parse(scriptSlice)
}
// Print category scores
console.log('=== CATEGORY SCORES ===')
for (const [id, cat] of Object.entries(lhr.categories || {})) {
console.log(` ${id}: ${Math.round((cat.score || 0) * 100)}`)
}
// Print failed/low audits (score < 1 and not null)
console.log('\n=== FAILED / LOW AUDITS ===')
for (const [id, audit] of Object.entries(lhr.audits || {})) {
if (audit.score === null || audit.score === undefined) continue
if (audit.score >= 1) continue
const items = audit.details?.items?.length || 0
const displayValue = audit.displayValue || ''
console.log(` [${(audit.score * 100).toFixed(0).padStart(3)}] ${id} ${displayValue}`)
if (items > 0 && audit.score < 0.9) {
const heads = (audit.details?.items || []).slice(0, 4)
heads.forEach((item) => {
const label = item.node?.snippet || item.url || item.description || item.label || JSON.stringify(item).slice(0, 120)
console.log(`${label}`)
})
}
}
// Print all opportunity audits
console.log('\n=== OPPORTUNITY AUDITS ===')
for (const [id, audit] of Object.entries(lhr.audits || {})) {
if (audit.details?.type !== 'opportunity') continue
const savings = audit.details?.overallSavingsMs || 0
if (savings < 100) continue
console.log(` ${id}: ${savings}ms savings`)
}

View File

@@ -0,0 +1,359 @@
#!/bin/bash
set -euo pipefail
remote_folder="${REMOTE_FOLDER:-/opt/www/virtual/SkinbaseNova}"
remote_server="${REMOTE_SERVER:-klevze@server3.klevze.si}"
remote_release_root="${REMOTE_RELEASE_ROOT:-${REMOTE_FOLDER:-/opt/www/virtual/SkinbaseNova}.releases}"
remote_shared_root="${REMOTE_SHARED_ROOT:-${REMOTE_RELEASE_ROOT:-/opt/www/virtual/SkinbaseNova.releases}/shared}"
php_bin="${PHP_BIN:-php}"
composer_bin="${COMPOSER_BIN:-composer}"
ssh_bin="${SSH_BIN:-ssh}"
dry_run=0
list_only=0
rollback_release_id=""
use_previous=0
skip_maintenance=0
usage() {
cat <<'EOF'
Usage: bash scripts/rollback-production.sh [options]
Options:
--previous Switch to the previous retained release on the production server.
--release-id ID Switch to a specific retained release ID.
--list List retained releases and exit.
--dry-run Preview the release switch without changing the active release.
--no-maintenance Skip php artisan down/up during the switch.
--help Show this help.
Environment overrides:
REMOTE_FOLDER, REMOTE_SERVER, REMOTE_RELEASE_ROOT, REMOTE_SHARED_ROOT, PHP_BIN, COMPOSER_BIN, SSH_BIN
Notes:
- Rollback changes the active server release; it does not re-upload files from local.
- Database migrations are not reversed automatically.
EOF
}
log_step() {
printf '\n==> %s\n' "$1"
}
log_info() {
printf ' -> %s\n' "$1"
}
die() {
printf 'ERROR: %s\n' "$1" >&2
exit 1
}
require_command() {
local command_name="$1"
local description="$2"
command -v "$command_name" >/dev/null 2>&1 || die "$description is required but was not found in PATH ($command_name)."
}
sanitize_release_fragment() {
local value="$1"
value="${value//[^A-Za-z0-9._-]/-}"
value="${value#-}"
value="${value%-}"
printf '%s' "$value"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--previous)
use_previous=1
;;
--release-id)
shift
rollback_release_id="$(sanitize_release_fragment "${1:?Missing value for --release-id}")"
;;
--release-id=*)
rollback_release_id="$(sanitize_release_fragment "${1#*=}")"
;;
--list)
list_only=1
;;
--dry-run)
dry_run=1
;;
--no-maintenance)
skip_maintenance=1
;;
--help|-h)
usage
exit 0
;;
*)
die "Unknown option: $1"
;;
esac
shift
done
require_command "$ssh_bin" "SSH"
if [[ "$list_only" -eq 0 && "$use_previous" -eq 0 && -z "$rollback_release_id" ]]; then
die "Choose either --previous or --release-id, or use --list to inspect retained releases."
fi
if [[ "$use_previous" -eq 1 && -n "$rollback_release_id" ]]; then
die "Use either --previous or --release-id, not both."
fi
if [[ "$list_only" -eq 1 ]]; then
log_step "Retained releases on $remote_server"
"$ssh_bin" "$remote_server" \
REMOTE_RELEASE_ROOT="$(printf '%q' "$remote_release_root")" \
'bash -s' <<'EOF'
set -euo pipefail
current_release="unknown"
if [[ -f "${REMOTE_RELEASE_ROOT}/current-release.txt" ]]; then
current_release="$(cat "${REMOTE_RELEASE_ROOT}/current-release.txt")"
fi
printf 'Current release: %s\n' "$current_release"
if [[ ! -d "${REMOTE_RELEASE_ROOT}/releases" ]]; then
printf 'No retained releases found in %s\n' "$REMOTE_RELEASE_ROOT"
exit 0
fi
find "${REMOTE_RELEASE_ROOT}/releases" -mindepth 1 -maxdepth 1 -type d | sort -r | while read -r path; do
release_id="$(basename "$path")"
marker=" "
if [[ "$release_id" == "$current_release" ]]; then
marker="*"
fi
printf '%s %s\n' "$marker" "$release_id"
done
EOF
exit 0
fi
log_step "Switching production release on $remote_server"
if [[ "$dry_run" -eq 1 ]]; then
log_info "Dry-run mode enabled; remote current release will not be changed"
fi
"$ssh_bin" "$remote_server" \
REMOTE_FOLDER="$(printf '%q' "$remote_folder")" \
REMOTE_RELEASE_ROOT="$(printf '%q' "$remote_release_root")" \
REMOTE_SHARED_ROOT="$(printf '%q' "$remote_shared_root")" \
PHP_BIN="$(printf '%q' "$php_bin")" \
COMPOSER_BIN="$(printf '%q' "$composer_bin")" \
ROLLBACK_RELEASE_ID="$(printf '%q' "$rollback_release_id")" \
USE_PREVIOUS="$use_previous" \
DRY_RUN="$dry_run" \
SKIP_MAINTENANCE="$skip_maintenance" \
'bash -s' <<'EOF'
set -euo pipefail
current_link="${REMOTE_RELEASE_ROOT}/current"
log_step() {
printf '\n==> %s\n' "$1"
}
log_info() {
printf ' -> %s\n' "$1"
}
log_warn() {
printf 'WARN: %s\n' "$1" >&2
}
die() {
printf 'ERROR: %s\n' "$1" >&2
exit 1
}
current_release_id() {
if [[ -L "$current_link" ]]; then
basename "$(readlink "$current_link")"
return 0
fi
printf '%s\n' ''
}
resolve_target_release() {
local current_release
local target_release
local -a releases=()
[[ -d "${REMOTE_RELEASE_ROOT}/releases" ]] || die "No retained releases exist under ${REMOTE_RELEASE_ROOT}/releases"
current_release="$(current_release_id)"
if [[ "$USE_PREVIOUS" -eq 1 ]]; then
mapfile -t releases < <(find "${REMOTE_RELEASE_ROOT}/releases" -mindepth 1 -maxdepth 1 -type d | sort -r)
for release_path in "${releases[@]}"; do
target_release="$(basename "$release_path")"
if [[ "$target_release" != "$current_release" ]]; then
printf '%s\n' "$target_release"
return 0
fi
done
die "No previous retained release is available."
fi
printf '%s\n' "$ROLLBACK_RELEASE_ID"
}
ensure_dir() {
mkdir -p "$1"
}
ensure_php_runtime_dir() {
local target_dir="$1"
local -a privileged_cmd=()
if command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1; then
privileged_cmd=(sudo -n)
elif [[ "$(id -u)" -eq 0 ]]; then
privileged_cmd=()
fi
if [[ ! -d "$target_dir" ]]; then
if [[ ${#privileged_cmd[@]} -gt 0 ]]; then
"${privileged_cmd[@]}" mkdir -p "$target_dir"
else
mkdir -p "$target_dir"
fi
fi
if [[ ${#privileged_cmd[@]} -gt 0 || "$(id -u)" -eq 0 ]]; then
"${privileged_cmd[@]}" chown -R skinbase:skinbase "$target_dir"
"${privileged_cmd[@]}" chmod 770 "$target_dir"
return
fi
chmod 770 "$target_dir" >/dev/null 2>&1 || true
}
link_shared_paths() {
local target_release="$1"
ensure_dir "$target_release/public" "$target_release/var"
rm -f "$target_release/.env"
ln -sfn "${REMOTE_SHARED_ROOT}/.env" "$target_release/.env"
rm -rf "$target_release/storage"
ln -sfn "${REMOTE_SHARED_ROOT}/storage" "$target_release/storage"
rm -rf "$target_release/public/files"
ln -sfn "${REMOTE_SHARED_ROOT}/public/files" "$target_release/public/files"
rm -rf "$target_release/public/sitemaps"
ln -sfn "${REMOTE_SHARED_ROOT}/public/sitemaps" "$target_release/public/sitemaps"
rm -rf "$target_release/var/php-tmp"
ln -sfn "${REMOTE_SHARED_ROOT}/var/php-tmp" "$target_release/var/php-tmp"
rm -rf "$target_release/var/php-sessions"
ln -sfn "${REMOTE_SHARED_ROOT}/var/php-sessions" "$target_release/var/php-sessions"
}
bring_app_up() {
if [[ "$SKIP_MAINTENANCE" -eq 0 && -f "$REMOTE_FOLDER/artisan" ]]; then
"$PHP_BIN" "$REMOTE_FOLDER/artisan" up >/dev/null 2>&1 || true
fi
}
target_release="$(resolve_target_release)"
[[ -n "$target_release" ]] || die "Unable to resolve a target release."
target_release_path="${REMOTE_RELEASE_ROOT}/releases/${target_release}"
[[ -d "$target_release_path" ]] || die "Retained release not found: ${target_release_path}"
current_release="$(current_release_id)"
[[ -n "$current_release" ]] || die "No active current release is configured under ${current_link}"
if [[ "$target_release" == "$current_release" ]]; then
die "Target release is already active: ${target_release}"
fi
ensure_php_runtime_dir "${REMOTE_SHARED_ROOT}/var/php-tmp"
ensure_php_runtime_dir "${REMOTE_SHARED_ROOT}/var/php-sessions"
link_shared_paths "$target_release_path"
[[ -f "${REMOTE_SHARED_ROOT}/.env" ]] || die "Shared production .env is missing at ${REMOTE_SHARED_ROOT}/.env"
[[ -f "${target_release_path}/artisan" ]] || die "Target release is missing artisan: ${target_release_path}/artisan"
log_info "Current release: ${current_release}"
log_info "Target release: ${target_release}"
log_info "Target path: ${target_release_path}"
if [[ "$DRY_RUN" -eq 1 ]]; then
exit 0
fi
trap bring_app_up EXIT
if [[ "$SKIP_MAINTENANCE" -eq 0 && -f "$REMOTE_FOLDER/artisan" ]]; then
log_step "Enabling maintenance mode"
"$PHP_BIN" "$REMOTE_FOLDER/artisan" down --retry=60 || true
fi
if [[ ! -f "${target_release_path}/vendor/autoload.php" ]]; then
log_step "Installing Composer dependencies in target release"
(
cd "$target_release_path"
"$COMPOSER_BIN" install --no-dev --prefer-dist --optimize-autoloader --no-interaction
)
fi
log_step "Switching current release to ${target_release}"
ln -sfn "$target_release_path" "$current_link"
ln -sfn "$current_link" "$REMOTE_FOLDER"
cd "$REMOTE_FOLDER"
log_step "Refreshing caches"
"$PHP_BIN" artisan view:clear
"$PHP_BIN" artisan optimize:clear
"$PHP_BIN" artisan optimize
"$PHP_BIN" artisan view:cache
if [[ "$SKIP_MAINTENANCE" -eq 0 ]]; then
log_step "Bringing application back online"
"$PHP_BIN" artisan up
trap - EXIT
fi
if ! "$PHP_BIN" artisan homepage:warm-guest-cache; then
log_warn "Homepage guest cache warm failed during rollback."
fi
if ! "$PHP_BIN" artisan posts:warm-trending; then
log_warn "Post trending cache warm failed during rollback."
fi
log_step "Restarting queue workers"
"$PHP_BIN" artisan queue:restart || true
cat > "${REMOTE_RELEASE_ROOT}/current-release.json" <<JSON
{
"release_id": "${target_release}",
"rolled_back_from_release_id": "${current_release}",
"rolled_back_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"remote_folder": "${REMOTE_FOLDER}",
"release_path": "${target_release_path}"
}
JSON
printf '%s\n' "${target_release}" > "${REMOTE_RELEASE_ROOT}/current-release.txt"
log_step "Release switch complete"
EOF