chore: dynamic UI sprint update and repo hygiene

This commit is contained in:
Clément SAILLANT
2026-03-02 20:23:05 +01:00
parent 6cbef0e7e5
commit a0255bca06
91 changed files with 8166 additions and 356 deletions
+16 -5
View File
@@ -2,13 +2,17 @@ import fs from 'node:fs';
import path from 'node:path';
import { execSync } from 'node:child_process';
const targetSiteUrl =
process.env.PUBLIC_SITE_URL ||
process.env.EXTERNAL_SITE_URL ||
'https://electron-rare.github.io/';
const rawSiteUrl = process.env.PUBLIC_SITE_URL || process.env.EXTERNAL_SITE_URL;
if (!rawSiteUrl) {
console.error(
'[build-astro-external] Missing PUBLIC_SITE_URL (or EXTERNAL_SITE_URL). Refusing external build with fallback canonical.'
);
process.exit(1);
}
const normalizeUrl = (value) => (value.endsWith('/') ? value : `${value}/`);
process.env.PUBLIC_SITE_URL = normalizeUrl(targetSiteUrl);
process.env.PUBLIC_SITE_URL = normalizeUrl(rawSiteUrl);
const siteUrl = process.env.PUBLIC_SITE_URL;
const run = (command) => {
execSync(command, {
@@ -33,4 +37,11 @@ if (fs.existsSync(labSource)) {
console.warn('[build-astro-external] Warning: lab/ not found; skipping lab route packaging.');
}
const robotsContent = `User-agent: *\nAllow: /\n\nSitemap: ${siteUrl}sitemap.xml\n`;
const sitemapContent = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n <url>\n <loc>${siteUrl}</loc>\n <changefreq>weekly</changefreq>\n <priority>1.0</priority>\n </url>\n <url>\n <loc>${siteUrl}lab/</loc>\n <changefreq>weekly</changefreq>\n <priority>0.8</priority>\n </url>\n</urlset>\n`;
fs.writeFileSync(path.resolve(process.cwd(), 'dist', 'robots.txt'), robotsContent, 'utf8');
fs.writeFileSync(path.resolve(process.cwd(), 'dist', 'sitemap.xml'), sitemapContent, 'utf8');
console.log('[build-astro-external] Wrote dist/robots.txt and dist/sitemap.xml for external domain.');
console.log('[build-astro-external] External Astro build done.');
+55
View File
@@ -0,0 +1,55 @@
import fs from 'node:fs';
import path from 'node:path';
const budgetRules = [
{
path: 'public/assets/da/openai/hero-pcb-routing-map-v2.webp',
maxBytes: 350 * 1024
},
{
path: 'public/assets/da/openai/hero-pcb-routing-map-mobile-low-noise-v2.webp',
maxBytes: 180 * 1024
},
{
path: 'public/assets/da/openai/hero-instrument-panel-v2.webp',
maxBytes: 220 * 1024
},
{
path: 'public/assets/da/openai/hero-instrument-panel-mobile-low-noise-v2.webp',
maxBytes: 200 * 1024
},
{
path: 'public/assets/da/openai/hero-carnet-labo-open-v2.webp',
maxBytes: 240 * 1024
},
{
path: 'public/assets/da/openai/carnet-labo-ouvert.webp',
maxBytes: 240 * 1024
}
];
const fmtKB = (bytes) => `${(bytes / 1024).toFixed(1)} KB`;
const failures = [];
for (const rule of budgetRules) {
const absPath = path.resolve(process.cwd(), rule.path);
if (!fs.existsSync(absPath)) {
failures.push(`${rule.path}: missing file`);
continue;
}
const size = fs.statSync(absPath).size;
if (size > rule.maxBytes) {
failures.push(`${rule.path}: ${fmtKB(size)} > budget ${fmtKB(rule.maxBytes)}`);
}
}
if (failures.length > 0) {
console.error('[image-budget] FAILED');
for (const failure of failures) {
console.error(`- ${failure}`);
}
process.exit(1);
}
console.log(`[image-budget] OK (${budgetRules.length} assets within budget)`);
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
set -euo pipefail
LOCAL_DIR="${1:-dist}"
FTP_URL="${FTP_URL:-}"
FTP_HOST="${FTP_HOST:-ftp.cluster129.hosting.ovh.net}"
FTP_USER="${FTP_USER:-ecobsoleiq}"
FTP_PORT="${FTP_PORT:-21}"
FTP_REMOTE_DIR="${FTP_REMOTE_DIR:-/www}"
FTP_ENV_FILE="${FTP_ENV_FILE:-.env.ftp}"
FTP_PASS="${FTP_PASS:-}"
if [[ -z "$FTP_PASS" && -n "${FTP_PASS_FILE:-}" && -f "$FTP_PASS_FILE" ]]; then
FTP_PASS="$(tr -d '\r\n' < "$FTP_PASS_FILE")"
fi
if [[ -z "$FTP_PASS" && -f "$FTP_ENV_FILE" ]]; then
# shellcheck disable=SC1090
source "$FTP_ENV_FILE"
fi
if [[ -z "${FTP_PASS:-}" ]]; then
echo "Missing FTP_PASS."
echo "Set it with: FTP_PASS='...'"
echo "or in: ${FTP_ENV_FILE} (local, non versioned)"
exit 1
fi
if [[ -n "$FTP_URL" ]]; then
if [[ "$FTP_URL" =~ ^ftp://([^@:/]+)@([^:/]+)(:([0-9]+))?(/.*)?$ ]]; then
FTP_USER="${BASH_REMATCH[1]}"
FTP_HOST="${BASH_REMATCH[2]}"
FTP_PORT="${BASH_REMATCH[4]:-$FTP_PORT}"
if [[ -n "${BASH_REMATCH[5]:-}" ]]; then
FTP_REMOTE_DIR="${BASH_REMATCH[5]}"
fi
else
echo "Invalid FTP_URL format. Expected ftp://user@host:21/path"
exit 1
fi
fi
if [[ -z "$FTP_REMOTE_DIR" ]]; then
FTP_REMOTE_DIR="/www"
fi
if [[ "$FTP_REMOTE_DIR" != /* ]]; then
FTP_REMOTE_DIR="/$FTP_REMOTE_DIR"
fi
if [[ "$FTP_REMOTE_DIR" == "/" && "${ALLOW_FTP_ROOT_SYNC:-0}" != "1" ]]; then
echo "Refusing FTP sync to '/'."
echo "Set FTP_REMOTE_DIR='/www' (recommended for OVH web hosting)."
echo "If you really need root sync, set ALLOW_FTP_ROOT_SYNC=1."
exit 1
fi
if [[ ! -d "$LOCAL_DIR" ]]; then
echo "Local directory not found: $LOCAL_DIR"
exit 1
fi
if ! command -v lftp >/dev/null 2>&1; then
echo "lftp is required. Install via: brew install lftp | apt-get install lftp"
exit 1
fi
cat <<EOF | lftp -u "$FTP_USER","$FTP_PASS" "$FTP_HOST"
set ftp:list-options -a;
set cmd:fail-exit true;
set net:max-retries 2;
set net:timeout 30;
set mirror:use-pget-n 4;
mirror --reverse --delete \
--dereference \
--continue \
--verbose \
"$LOCAL_DIR" "$FTP_REMOTE_DIR"
bye
EOF
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env bash
set -euo pipefail
LOCAL_DIR="${1:-dist}"
FTP_HOST="${FTP_HOST:-ftp.cluster129.hosting.ovh.net}"
FTP_USER="${FTP_USER:-ecobsoleiq}"
FTP_REMOTE_DIR="${FTP_REMOTE_DIR:-/home/ecobsoleiq/www}"
FTP_ENV_FILE="${FTP_ENV_FILE:-.env.ftp}"
FTP_PASS="${FTP_PASS:-}"
FTP_URL="${FTP_URL:-}"
DEPLOY_SFTP_DELETE="${DEPLOY_SFTP_DELETE:-0}"
if [[ -z "$FTP_PASS" && -n "${FTP_PASS_FILE:-}" && -f "$FTP_PASS_FILE" ]]; then
FTP_PASS="$(tr -d '\r\n' < "$FTP_PASS_FILE")"
fi
if [[ -z "$FTP_PASS" && -f "$FTP_ENV_FILE" ]]; then
# shellcheck disable=SC1090
source "$FTP_ENV_FILE"
fi
if [[ -z "${FTP_PASS:-}" ]]; then
echo "Missing FTP_PASS."
echo "Set it with: FTP_PASS='...'"
echo "or in: ${FTP_ENV_FILE} (local, non versioned)"
exit 1
fi
if [[ -n "$FTP_URL" ]]; then
if [[ "$FTP_URL" =~ ^s?ftp://([^@:/]+)@([^:/]+)(:([0-9]+))?(/.*)?$ ]]; then
FTP_USER="${BASH_REMATCH[1]}"
FTP_HOST="${BASH_REMATCH[2]}"
fi
fi
if [[ -z "$FTP_REMOTE_DIR" ]]; then
FTP_REMOTE_DIR="/home/ecobsoleiq/www"
fi
if [[ ! -d "$LOCAL_DIR" ]]; then
echo "Local directory not found: $LOCAL_DIR"
exit 1
fi
if ! command -v lftp >/dev/null 2>&1; then
echo "lftp is required. Install via: brew install lftp | apt-get install lftp"
exit 1
fi
SYNC_DIRS=("assets" "_astro" "lab")
ROOT_FILES=("index.html" "robots.txt" "sitemap.xml" "favicon.ico" "favicon.svg" "favicon-16x16.png" "favicon-32x32.png" "apple-touch-icon.png")
if [[ "$DEPLOY_SFTP_DELETE" != "1" ]]; then
mirror_delete_flag=""
else
mirror_delete_flag="--delete"
fi
CMD_FILE="$(mktemp)"
trap 'rm -f "$CMD_FILE"' EXIT
cat > "$CMD_FILE" <<'EOF'
set net:timeout 120
set sftp:connect-program "ssh -a -x -o ConnectTimeout=20 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
set cmd:fail-exit true
set net:max-retries 6
set net:reconnect-interval-base 5
set net:reconnect-interval-max 30
set mirror:use-pget-n 1
set xfer:clobber on
EOF
printf 'open -u "%s","%s" "sftp://%s"\n' "$FTP_USER" "$FTP_PASS" "$FTP_HOST" >> "$CMD_FILE"
# Sync known directories first (stable on OVH shared hosting).
for sync_dir in "${SYNC_DIRS[@]}"; do
local_dir="$LOCAL_DIR/$sync_dir"
remote_dir="$FTP_REMOTE_DIR/$sync_dir"
if [[ -d "$local_dir" ]]; then
printf 'mirror --reverse --dereference --continue --only-newer %s --verbose "%s" "%s"\n' \
"$mirror_delete_flag" "$local_dir" "$remote_dir" >> "$CMD_FILE"
else
echo "Skipping missing directory: $local_dir" >&2
fi
done
# Sync root assets one-by-one to avoid long-running full-root mirror timeouts.
for root_file in "${ROOT_FILES[@]}"; do
if [[ -f "$LOCAL_DIR/$root_file" ]]; then
printf 'put -O "%s" "%s"\n' "$FTP_REMOTE_DIR" "$LOCAL_DIR/$root_file" >> "$CMD_FILE"
else
echo "Skipping missing root file: $LOCAL_DIR/$root_file" >&2
fi
done
echo "bye" >> "$CMD_FILE"
if [[ "${DEPLOY_SFTP_DRYRUN:-0}" == "1" ]]; then
echo "--- lftp command plan ---"
cat "$CMD_FILE"
exit 0
fi
if [[ -n "${DEPLOY_SFTP_VERBOSE:-}" ]]; then
echo "--- lftp command plan ---"
cat "$CMD_FILE"
fi
lftp -f "$CMD_FILE"
+321
View File
@@ -0,0 +1,321 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TODAY="$(date +%F)"
BATCH_FILE="${1:-$ROOT_DIR/tmp/imagegen/mistral/carnet-labo-assets-v2.jsonl}"
OUTPUT_DIR="${OUTPUT_DIR:-$ROOT_DIR/output/imagegen/mistral/carnet-labo-assets-v2}"
REPORT_FILE="${REPORT_FILE:-$ROOT_DIR/artifacts/imagegen/$TODAY/asset-run-report.md}"
MISTRAL_IMAGES_ENDPOINT="https://api.mistral.ai/v1/images/generations"
MISTRAL_CONVERSATIONS_ENDPOINT="https://api.mistral.ai/v1/conversations"
MISTRAL_AGENTS_ENDPOINT="https://api.mistral.ai/v1/agents"
MISTRAL_FILES_ENDPOINT="https://api.mistral.ai/v1/files"
MISTRAL_MODELS_ENDPOINT="https://api.mistral.ai/v1/models"
ENV_FILE="$ROOT_DIR/tmp/imagegen/.env.mistral"
AGENT_CACHE_FILE="$ROOT_DIR/tmp/imagegen/.mistral-image-agent.id"
if [[ -f "$ENV_FILE" ]]; then
# shellcheck disable=SC1090
source "$ENV_FILE"
fi
MODEL="${MISTRAL_IMAGE_MODEL:-mistral-medium-2505}"
AGENT_ID="${MISTRAL_IMAGE_AGENT_ID:-}"
mkdir -p "$OUTPUT_DIR" "$(dirname "$REPORT_FILE")"
if [[ "${1-}" == "--list-models" ]]; then
if [[ -z "${MISTRAL_API_KEY:-}" ]]; then
echo "MISTRAL_API_KEY is required."
exit 1
fi
curl -sS "$MISTRAL_MODELS_ENDPOINT" \
-H "Authorization: Bearer $MISTRAL_API_KEY" \
-H "Content-Type: application/json" | jq .
exit 0
fi
if [[ "${1-}" == "--reset-agent" ]]; then
rm -f "$AGENT_CACHE_FILE"
shift || true
BATCH_FILE="${1:-$ROOT_DIR/tmp/imagegen/mistral/carnet-labo-assets-v2.jsonl}"
fi
if [[ -z "${MISTRAL_API_KEY:-}" ]]; then
echo "MISTRAL_API_KEY is required."
exit 1
fi
if [[ ! -f "$BATCH_FILE" ]]; then
echo "Missing batch file: $BATCH_FILE"
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "jq is required."
exit 1
fi
if ! command -v curl >/dev/null 2>&1; then
echo "curl is required."
exit 1
fi
echo "# Asset Run Report ($TODAY)" > "$REPORT_FILE"
echo >> "$REPORT_FILE"
echo "- Batch: \`$BATCH_FILE\`" >> "$REPORT_FILE"
echo "- Output: \`$OUTPUT_DIR\`" >> "$REPORT_FILE"
echo "- Model: \`$MODEL\`" >> "$REPORT_FILE"
echo >> "$REPORT_FILE"
echo "| Asset | Provider | Status | Note |" >> "$REPORT_FILE"
echo "|---|---|---|---|" >> "$REPORT_FILE"
log_report() {
local asset="$1"
local provider="$2"
local status="$3"
local note="$4"
echo "| $asset | $provider | $status | $note |" >> "$REPORT_FILE"
}
post_json() {
local url="$1"
local payload="$2"
local out_file="$3"
local http_code
http_code="$(curl -sS -o "$out_file" -w "%{http_code}" -X POST "$url" \
-H "Authorization: Bearer $MISTRAL_API_KEY" \
-H "Content-Type: application/json" \
-d "$payload")"
echo "$http_code"
}
extract_file_ids_from_conversation() {
local json_file="$1"
jq -r '
.outputs // [] |
map(select(.type == "message.output")) |
.[].content // [] |
map(select(.type == "tool_file" and .file_id != null and .file_id != "")) |
.[].file_id
' "$json_file" 2>/dev/null | tr -d "\\r" | sed "/^$/d"
}
extract_first_image_url() {
local json_file="$1"
jq -r '.data[0].url // empty' "$json_file" 2>/dev/null
}
ensure_agent() {
if [[ -n "$AGENT_ID" ]]; then
return 0
fi
if [[ -f "$AGENT_CACHE_FILE" ]]; then
local cached
cached="$(tr -d '[:space:]' < "$AGENT_CACHE_FILE")"
if [[ -n "$cached" ]]; then
AGENT_ID="$cached"
return 0
fi
fi
local payload tmpfile http_code
payload="$(jq -nc \
--arg model "$MODEL" \
'{model:$model,name:"Image Generation Agent",description:"Image generation fallback agent",instructions:"Use image_generation for image requests.",tools:[{type:"image_generation"}]}')"
tmpfile="$(mktemp)"
http_code="$(post_json "$MISTRAL_AGENTS_ENDPOINT" "$payload" "$tmpfile")"
if [[ "$http_code" != 2* ]] || jq -e '.error' "$tmpfile" >/dev/null 2>&1; then
echo "Unable to create Mistral fallback agent."
cat "$tmpfile"
rm -f "$tmpfile"
return 1
fi
AGENT_ID="$(jq -r '.id // empty' "$tmpfile")"
rm -f "$tmpfile"
if [[ -z "$AGENT_ID" ]]; then
return 1
fi
echo "$AGENT_ID" > "$AGENT_CACHE_FILE"
export MISTRAL_IMAGE_AGENT_ID="$AGENT_ID"
}
copy_from_local_fallback() {
local out="$1"
local target="$2"
local cache1="$ROOT_DIR/output/imagegen/openai/carnet-labo-assets-v1/$out"
local cache2="$ROOT_DIR/public/assets/da/openai/$out"
if [[ -f "$cache1" ]]; then
cp "$cache1" "$target"
return 0
fi
if [[ -f "$cache2" ]]; then
cp "$cache2" "$target"
return 0
fi
return 1
}
download_mistral_file_id() {
local file_id="$1"
local target="$2"
curl -L -sS "$MISTRAL_FILES_ENDPOINT/$file_id/content" \
-H "Authorization: Bearer $MISTRAL_API_KEY" \
-o "$target"
}
generate_with_images_api() {
local prompt="$1"
local size="$2"
local out_tmp="$3"
local payload
payload="$(jq -nc --arg model "$MODEL" --arg prompt "$prompt" --arg size "$size" '{model:$model,prompt:$prompt,n:1,size:$size}')"
post_json "$MISTRAL_IMAGES_ENDPOINT" "$payload" "$out_tmp"
}
generate_with_agent_api() {
local prompt="$1"
local out_tmp="$2"
local payload
ensure_agent || return 1
payload="$(jq -nc --arg agent_id "$AGENT_ID" --arg prompt "$prompt" '{agent_id:$agent_id,inputs:$prompt,stream:false}')"
post_json "$MISTRAL_CONVERSATIONS_ENDPOINT" "$payload" "$out_tmp"
}
echo "Backend: Mistral image generation"
echo "Model: $MODEL"
echo "Batch: $BATCH_FILE"
echo "Output: $OUTPUT_DIR"
echo "Report: $REPORT_FILE"
echo
tmpfile="$(mktemp)"
trap 'rm -f "$tmpfile"' EXIT
success_count=0
fallback_count=0
error_count=0
while IFS= read -r line; do
[[ -z "$line" ]] && continue
out="$(echo "$line" | jq -r '.out // ""')"
prompt="$(echo "$line" | jq -r '.prompt // ""')"
size="$(echo "$line" | jq -r '.size // "1024x1024"')"
guardrails="$(echo "$line" | jq -r '.style_guardrails // ""')"
target="$OUTPUT_DIR/$out"
if [[ -z "$out" || -z "$prompt" ]]; then
continue
fi
case "$size" in
256x256|512x512|1024x1024) ;;
*) size="1024x1024" ;;
esac
if [[ -n "$guardrails" ]]; then
prompt="$prompt Constraints: $guardrails"
fi
if [[ -f "$target" ]]; then
log_report "$out" "cache-output" "ok" "already exists"
((success_count+=1))
continue
fi
provider=""
status="failed"
note=""
# Stage 1: images/generations with exponential retry on 503
for attempt in 1 2 3 4; do
http_code="$(generate_with_images_api "$prompt" "$size" "$tmpfile")"
if [[ "$http_code" == "503" ]]; then
sleep $((2 ** (attempt - 1)))
continue
fi
if [[ "$http_code" == 2* ]] && ! jq -e '.error' "$tmpfile" >/dev/null 2>&1; then
url="$(extract_first_image_url "$tmpfile")"
if [[ -n "$url" ]]; then
if curl -L -sS "$url" -o "$target"; then
provider="mistral-images"
status="ok"
note="generated via /v1/images/generations"
fi
fi
fi
break
done
# Stage 2: fallback to agent/conversations
if [[ "$status" != "ok" ]]; then
for attempt in 1 2 3 4; do
http_code="$(generate_with_agent_api "$prompt" "$tmpfile" || echo "000")"
if [[ "$http_code" == "503" ]]; then
sleep $((2 ** (attempt - 1)))
continue
fi
if [[ "$http_code" == 2* ]] && ! jq -e '.error' "$tmpfile" >/dev/null 2>&1; then
file_id="$(extract_file_ids_from_conversation "$tmpfile" | head -n 1)"
if [[ -n "$file_id" ]]; then
if download_mistral_file_id "$file_id" "$target"; then
provider="mistral-agent"
status="ok"
note="generated via /v1/conversations fallback"
fi
fi
fi
break
done
fi
# Stage 3: fallback to local cache
if [[ "$status" != "ok" ]]; then
if copy_from_local_fallback "$out" "$target"; then
provider="local-cache"
status="ok"
note="fallback from openai/public cache"
((fallback_count+=1))
else
provider="none"
status="failed"
note="no provider available"
((error_count+=1))
fi
fi
if [[ "$status" == "ok" ]]; then
((success_count+=1))
fi
log_report "$out" "$provider" "$status" "$note"
echo "$status: $out ($provider)"
done < "$BATCH_FILE"
echo >> "$REPORT_FILE"
echo "- Success: $success_count" >> "$REPORT_FILE"
echo "- Local-cache fallback: $fallback_count" >> "$REPORT_FILE"
echo "- Errors: $error_count" >> "$REPORT_FILE"
if (( error_count > 0 )); then
echo "Completed with errors. See $REPORT_FILE"
exit 1
fi
echo "Done. Report: $REPORT_FILE"
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
"$SCRIPT_DIR/gen-carnet-labo-assets-mistral.sh" "${SCRIPT_DIR}/../tmp/imagegen/mistral/carnet-labo-assets-v2.jsonl"