7 Commits
Author SHA1 Message Date
Jordan Diaz dabdfa1354 Merge feat/regla-visibilidad-unica: header.php y robots consultan la regla única de builder_saas 2026-10-14 15:27:50 +00:00
Acai ForgeandClaude Opus 5.5 bb532515ce feat: header.php y robots consultan la regla única de builder_saas
header.php decide con acaiVisibilidadWeb (y 503 + Retry-After con
acaiCabecerasMantenimiento) si el plugin la trae; si no, la reserva en
línea de siempre. cms/robots.php cierra con Disallow: / mientras la web no
esté publicada (acaiRobotsBloqueados); un robots.txt propio manda.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 15:27:40 +00:00
Acai ForgeandClaude Opus 5.5 d1235f2e20 Merge fix/cache-sin-redis: sin Redis la web sigue sin caché en vez de morir
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-03 15:27:20 +00:00
Acai ForgeandClaude Opus 5.5 cb03ecd239 fix(cache): sin Redis la web sigue sin caché en vez de morir
initCache() de CocoDB y AcaiAPI conectaba sin capturar errores: en un
hosting sin Redis (o sin la extensión) el plugin cache_handler tumbaba la
web entera con un RedisException. Ahora se marca como no disponible para
el resto de la petición y cacheGet/cacheSet, que ya comprobaban la
conexión, no hacen nada. Con Redis el comportamiento no cambia.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 17:08:52 +00:00
Jordan DiazandClaude Opus 5.5 80b6541e30 fix: crear uploads/webp sin carrera en funciones.php
Varias peticiones a la vez (el prerender de Astro las lanza en paralelo) veian
la carpeta sin crear y la segunda imprimia "mkdir(): File exists" delante del
JSON de action_ws, rompiendo el build de una web recien descargada.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 15:16:28 +00:00
Acai ForgeandClaude Opus 5.5 775ae1c95d Merge feat/uploads-columnas-miniaturas: fotos de registros con dimensiones y miniaturas
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 20:50:36 +00:00
Acai ForgeandClaude Opus 5.5 49522987dd feat(core): las fotos de los registros traen dimensiones y miniaturas
AcaiAPI::batchLoadUploads (camino de cmsApi get con uploads:true, el que usan
los listados y fichas de las webs Astro) solo devolvia urlPath e info*: sin
width/height el front no reserva el hueco (CLS) y sin thumbUrlPath..4 no puede
montar un srcset. Ahora selecciona tambien width, height y URL+dimensiones de
los 4 huecos de miniatura (no las rutas de disco), y useAbsoluteUrls reescribe
tambien las URLs de las miniaturas.

Solo pide las columnas que existen en {prefix}uploads (SHOW COLUMNS una vez por
prefijo y peticion): una web antigua sin alguna no se queda sin fotos.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 18:28:21 +00:00
5 changed files with 130 additions and 19 deletions
+77 -9
View File
@@ -15,6 +15,7 @@ class AcaiAPI
private static $schemaCache = []; private static $schemaCache = [];
private static $queryCaches = []; // list/query relation results private static $queryCaches = []; // list/query relation results
private static $requestGetCache = []; // unconditional per-request deduplication private static $requestGetCache = []; // unconditional per-request deduplication
private static $uploadsColumnsCache = []; // prefix => [columna => true] de {prefix}uploads
// ── Public caches (mirror CocoDB interface) ─────────────────────── // ── Public caches (mirror CocoDB interface) ───────────────────────
public static $getCaches = []; // in-memory result cache for get() public static $getCaches = []; // in-memory result cache for get()
@@ -33,6 +34,7 @@ class AcaiAPI
// ── Cache flags (mirror CocoDB interface) ───────────────────────── // ── Cache flags (mirror CocoDB interface) ─────────────────────────
public static $redis = null; public static $redis = null;
public static $redis_expireTime = 0; public static $redis_expireTime = 0;
public static $redis_no_disponible = false;
public static $force_load_cache = false; public static $force_load_cache = false;
public static $force_redis = false; public static $force_redis = false;
public static $force_redis_module = false; // consumed by external cache layer public static $force_redis_module = false; // consumed by external cache layer
@@ -646,14 +648,29 @@ class AcaiAPI
/** Connect to Redis (idempotent). */ /** Connect to Redis (idempotent). */
static function initCache() static function initCache()
{ {
// Sin Redis (hosting propio sin el servicio o sin la extensión, o el
// servicio caído) la web sigue SIN caché en vez de morir con un fatal:
// cacheGet/cacheSet ya no hacen nada si no hay conexión. Se marca para
// no reintentar en cada consulta de la misma petición.
if (self::$redis_no_disponible) return;
if (!class_exists('Redis')) {
self::$redis_no_disponible = true;
self::$redis = null;
return;
}
$redisHost = getenv('REDIS_HOST') ?: '127.0.0.1'; $redisHost = getenv('REDIS_HOST') ?: '127.0.0.1';
$redisPort = (int)(getenv('REDIS_PORT') ?: 6379); $redisPort = (int)(getenv('REDIS_PORT') ?: 6379);
if (!self::$redis) { try {
self::$redis = new Redis(); if (!self::$redis) self::$redis = new Redis();
self::$redis->connect($redisHost, $redisPort); if (!self::$redis->isConnected() && !self::$redis->connect($redisHost, $redisPort)) {
} elseif (!self::$redis->isConnected()) { throw new RuntimeException("no se pudo conectar a $redisHost:$redisPort");
self::$redis->connect($redisHost, $redisPort); }
} catch (Throwable $e) {
self::$redis_no_disponible = true;
self::$redis = null;
error_log('[cache] Redis no disponible, se sigue sin caché: ' . $e->getMessage());
} }
} }
@@ -855,7 +872,15 @@ class AcaiAPI
$db = Db::getInstance(); $db = Db::getInstance();
$ph = implode(',', array_fill(0, count($nums), '?')); $ph = implode(',', array_fill(0, count($nums), '?'));
$fieldPh = implode(',', array_fill(0, count($uploadFields), '?')); $fieldPh = implode(',', array_fill(0, count($uploadFields), '?'));
// width/height y las miniaturas (URL + dimensiones de los 4 huecos) van
// con cada foto: sin ellas el front no puede reservar el hueco (CLS) ni
// montar un srcset. Las rutas de disco (thumbFilePath*) no salen. Solo
// las que existan en la tabla: una web antigua sin alguna no se queda
// sin fotos por un SELECT roto.
$extra = self::existingUploadsColumns(
array_merge(['width', 'height'], self::uploadThumbColumns()), $prefix);
$selectCols = 'num, `order`, tableName, fieldName, recordNum, filePath, urlPath, ' $selectCols = 'num, `order`, tableName, fieldName, recordNum, filePath, urlPath, '
. ($extra ? '`' . implode('`, `', $extra) . '`, ' : '')
. 'info1, info2, info3, info4, info5, alt'; . 'info1, info2, info3, info4, info5, alt';
$params = array_merge(array_values($nums), [$tableName], array_values($uploadFields)); $params = array_merge(array_values($nums), [$tableName], array_values($uploadFields));
@@ -876,10 +901,12 @@ class AcaiAPI
// Re-index for O(1) lookup // Re-index for O(1) lookup
$index = []; $index = [];
foreach ($uploads as $upload) { foreach ($uploads as $upload) {
if ($options['useAbsoluteUrls'] && isset($upload['urlPath']) if ($options['useAbsoluteUrls']) {
&& strpos($upload['urlPath'], '/cms') === 0 foreach (array_merge(['urlPath'], self::uploadThumbUrlColumns()) as $col) {
) { if (isset($upload[$col]) && strpos($upload[$col], '/cms') === 0) {
$upload['urlPath'] = 'https://' . $_SERVER['HTTP_HOST'] . $upload['urlPath']; $upload[$col] = 'https://' . $_SERVER['HTTP_HOST'] . $upload[$col];
}
}
} }
$field = $upload['fieldName']; $field = $upload['fieldName'];
$recNum = $upload['recordNum']; $recNum = $upload['recordNum'];
@@ -888,6 +915,47 @@ class AcaiAPI
return $index; return $index;
} }
/** Sufijos de los 4 huecos de miniatura de `uploads` ('' = el primero). */
const UPLOAD_THUMB_SLOTS = ['', '2', '3', '4'];
/** Columnas de URL de las miniaturas: thumbUrlPath, thumbUrlPath2..4. */
private static function uploadThumbUrlColumns()
{
return array_map(function ($n) { return "thumbUrlPath$n"; }, self::UPLOAD_THUMB_SLOTS);
}
/** URL + ancho + alto de cada hueco de miniatura (sin rutas de disco). */
private static function uploadThumbColumns()
{
$cols = [];
foreach (self::UPLOAD_THUMB_SLOTS as $n) {
array_push($cols, "thumbUrlPath$n", "thumbWidth$n", "thumbHeight$n");
}
return $cols;
}
/**
* De `$wanted`, las columnas que existen en `{$prefix}uploads` (en su
* orden). Un SHOW COLUMNS por prefijo y petición; si falla, ninguna: el
* SELECT vuelve a las columnas de siempre en vez de romperse.
*/
private static function existingUploadsColumns(array $wanted, $prefix)
{
if (!isset(self::$uploadsColumnsCache[$prefix])) {
$existing = [];
try {
foreach ((array) Db::getInstance()->executeS("SHOW COLUMNS FROM `{$prefix}uploads`") as $col) {
if (isset($col['Field'])) $existing[$col['Field']] = true;
}
} catch (Exception $e) {
error_log('[AcaiAPI] SHOW COLUMNS de ' . $prefix . 'uploads fallido: ' . $e->getMessage());
}
self::$uploadsColumnsCache[$prefix] = $existing;
}
$cols = self::$uploadsColumnsCache[$prefix];
return array_values(array_filter($wanted, function ($c) use ($cols) { return isset($cols[$c]); }));
}
/** /**
* Injects upload arrays from the pre-built index into a single record row. * Injects upload arrays from the pre-built index into a single record row.
* Each upload field becomes an array of upload objects (same as CocoDB). * Each upload field becomes an array of upload objects (same as CocoDB).
+21 -5
View File
@@ -14,6 +14,7 @@ class CocoDB
public static $trackData = []; public static $trackData = [];
public static $redis = null; public static $redis = null;
public static $redis_expireTime = 0; public static $redis_expireTime = 0;
public static $redis_no_disponible = false;
public static $force_load_cache = false; public static $force_load_cache = false;
public static $force_redis = false; public static $force_redis = false;
public static $force_redis_module = false; public static $force_redis_module = false;
@@ -1176,14 +1177,29 @@ class CocoDB
*/ */
static function initCache() static function initCache()
{ {
// Sin Redis (hosting propio sin el servicio o sin la extensión, o el
// servicio caído) la web sigue SIN caché en vez de morir con un fatal:
// cacheGet/cacheSet ya no hacen nada si no hay conexión. Se marca para
// no reintentar en cada consulta de la misma petición.
if (self::$redis_no_disponible) return;
if (!class_exists('Redis')) {
self::$redis_no_disponible = true;
self::$redis = null;
return;
}
$redisHost = getenv('REDIS_HOST') ?: '127.0.0.1'; $redisHost = getenv('REDIS_HOST') ?: '127.0.0.1';
$redisPort = (int)(getenv('REDIS_PORT') ?: 6379); $redisPort = (int)(getenv('REDIS_PORT') ?: 6379);
if (!self::$redis) { try {
self::$redis = new Redis(); if (!self::$redis) self::$redis = new Redis();
self::$redis->connect($redisHost, $redisPort); if (!self::$redis->isConnected() && !self::$redis->connect($redisHost, $redisPort)) {
} else if (!self::$redis->isConnected()) { throw new RuntimeException("no se pudo conectar a $redisHost:$redisPort");
self::$redis->connect($redisHost, $redisPort); }
} catch (Throwable $e) {
self::$redis_no_disponible = true;
self::$redis = null;
error_log('[cache] Redis no disponible, se sigue sin caché: ' . $e->getMessage());
} }
} }
+9 -1
View File
@@ -20,7 +20,15 @@ if (file_exists($filePath)) {
# <?=strtoupper($configuracionRecord["tienda_nombre_empresa"]);?> # <?=strtoupper($configuracionRecord["tienda_nombre_empresa"]);?>
User-agent: * User-agent: *
<? if (strpos($_SERVER["HTTP_HOST"], "plandeweb.com")) {?> <?
// Cerrada mientras no este publicada: la regla es `acaiRobotsBloqueados` del
// plugin builder_saas (la misma de header.php y de la puerta de las webs
// Astro). Un robots.txt propio de la web manda sobre esto (arriba). Sin el
// plugin, como siempre: solo se cierran los dominios de pruebas.
$robotsBloqueados = strpos($_SERVER["HTTP_HOST"], "plandeweb.com")
|| (function_exists("acaiRobotsBloqueados")
&& acaiRobotsBloqueados(is_file(dirname(__DIR__) . "/.acai"), @$configuracionRecord['pagina_publicada']));
if ($robotsBloqueados) {?>
Disallow: / Disallow: /
<? }?> <? }?>
<? if (@$configuracionRecord['pagina_publicada']) {?> <? if (@$configuracionRecord['pagina_publicada']) {?>
+5 -2
View File
@@ -7,8 +7,11 @@ $modulosCargados = array();
$recursosCSS = array(); $recursosCSS = array();
// HASTA AUQI // HASTA AUQI
if (!file_exists(__DIR__.'/'.CMS_FOLDER.'/uploads/webp/')) { // Sin carrera: varias peticiones a la vez (el prerender de Astro las lanza en
mkdir(__DIR__.'/'.CMS_FOLDER.'/uploads/webp/'); // paralelo) veian la carpeta sin crear y la segunda imprimia "mkdir(): File
// exists" delante del JSON de action_ws, rompiendo el build de una web nueva.
if (!is_dir(__DIR__.'/'.CMS_FOLDER.'/uploads/webp/')) {
@mkdir(__DIR__.'/'.CMS_FOLDER.'/uploads/webp/', 0775, true);
} }
global $configuracionTienda; global $configuracionTienda;
$configuracionRecord = CocoDB::get("configuracion", "", null, 1); $configuracionRecord = CocoDB::get("configuracion", "", null, 1);
+18 -2
View File
@@ -78,16 +78,32 @@ $config_header = array(
// prod + publicada -> se ve // prod + publicada -> se ve
// prod + sin publicar + ?pruebas -> se ve, con el cartel rojo // prod + sin publicar + ?pruebas -> se ve, con el cartel rojo
// prod + sin publicar + visitante -> mantenimiento // prod + sin publicar + visitante -> mantenimiento
//
// La tabla de arriba es UNA regla para todas las webs (PHP y Astro) y para el
// robots.txt: `acaiVisibilidadWeb` del plugin builder_saas (ws_respond.php),
// que se reparte entero a todas las webs. Se llama con function_exists por la
// misma razon que la señal va en linea: si una web no trae el plugin, se
// queda con la RESERVA de abajo, que es la regla de siempre (y el cartel con
// 200, sin las cabeceras 503 de `acaiCabecerasMantenimiento`).
$esEntornoLocal = is_file(dirname(__FILE__) . "/.acai"); $esEntornoLocal = is_file(dirname(__FILE__) . "/.acai");
if ($esEntornoLocal || @$configuracionRecord["pagina_publicada"] || isset($_SESSION["pruebas"])) { if (function_exists("acaiVisibilidadWeb")) {
$visibilidadWeb = acaiVisibilidadWeb($esEntornoLocal, @$configuracionRecord["pagina_publicada"], isset($_SESSION["pruebas"]));
} else {
// RESERVA (sin el plugin): la misma tabla, en linea.
$visibilidadWeb = ($esEntornoLocal || @$configuracionRecord["pagina_publicada"]) ? "visible"
: (isset($_SESSION["pruebas"]) ? "pruebas" : "mantenimiento");
}
if ($visibilidadWeb !== "mantenimiento") {
if (@$customCode) echo modulo('custom-header',$config_header); else echo tpl('cabecera',$config_header); if (@$customCode) echo modulo('custom-header',$config_header); else echo tpl('cabecera',$config_header);
if (!$esEntornoLocal && isset($_SESSION["pruebas"]) && @$configuracionRecord["pagina_publicada"] == 0) { if ($visibilidadWeb === "pruebas") {
?> ?>
<div style="position:absolute;top:0px;width:150px;background-color:red;color:#fff;font-size:12px;padding:0 10px;z-index:99999999;transform:translateZ(1000px);">SITIO EN PRUEBAS</div> <div style="position:absolute;top:0px;width:150px;background-color:red;color:#fff;font-size:12px;padding:0 10px;z-index:99999999;transform:translateZ(1000px);">SITIO EN PRUEBAS</div>
<? <?
} }
} else { } else {
if (function_exists("acaiCabecerasMantenimiento")) acaiCabecerasMantenimiento();
die(tpl('mantenimiento',$config_header)); die(tpl('mantenimiento',$config_header));
} }
?> ?>