fix: cerrar la integracion 0.9.5 — recursos que faltaban y regresiones

Una revision a fondo del arbol contra 0.9.5 saco varios fallos, unos heredados y
otros que introdujo la propia integracion. Verificados uno a uno antes de tocar.

Recursos referenciados que no existian (los introduje al adoptar vistas de 0.9.5
que los usan, mientras la rama los habia borrado):
- Los cuatro temas *-SNH.css. settings_view ofrece Dark, Clear, Matrix y Purple,
  pero solo estaba OasisMobile: elegir cualquiera daba 404 y la app se quedaba sin
  estilos. Ahora los cuatro responden 200.
- pdf.min.mjs, pdf.worker.min.mjs y pdf-viewer.js, referenciados desde seis vistas.

Regresiones revertidas a 0.9.5 verbatim en catorce ficheros. Las de fondo:
- larp_model perdio getGoverningPeriodId, que backend.js llama: el anuncio de
  gobierno no salia nunca, con el TypeError tragado por un catch.
- courts_view y search_view tenian selected: false, que hyperscript SI serializa
  como atributo y HTML interpreta como seleccionado. La forma de upstream, el
  spread condicional, es la correcta.
- pm_model perdio includeDeleted, projects_model la validacion de deadline pasado,
  jobs_model el desempate por timestamp, middleware el flag de debug HTTP.

forum_view pasa a 0.9.5 conservando el filtro hot. Esto arregla un fallo activo: la
vista de la rama declaraba un cuarto parametro topic y backend.js llama con cinco
argumentos, asi que al abrir una respuesta el %clave del hilo se colaba como tema y
el hilo salia VACIO. Upstream pasa los mismos cinco argumentos a una firma de tres y
JS los ignora, que es lo correcto.

chats_view pasa a 0.9.5 y se implementa replyTo de verdad. Estaba muerto de punta a
punta: backend.js no leia ctx.query.replyTo ni pasaba el cuarto argumento a
sendMessage, asi que ningun mensaje llegaba a tener replyTo y la cita no se
renderizaba jamas. Ahora la cadena esta completa —modelo, GET, POST y vista— con
indice msgById armado antes de mezclar con las encuestas, y sin gate de movil:
responder a un mensaje no es una funcion de movil.

aria-label: encontrada la causa. No es que hyperaxe lo ignore, es que hyperscript
solo asigna como propiedad y html-element solo serializa atributos de su tabla, en
la que aria-* no esta; data-* si tiene rama propia, por eso las tablas responsive
funcionaban. Se arregla con el escape hatch attrs:{}, sin tocar node_modules.

prepare.sh copia README.md y falla si no esta: backend.js lo lee con readFileSync
sin try/catch, de modo que sin el fichero el backend muere con ENOENT al arrancar.

Verificado con la app en marcha: 17 rutas OK, los cuatro recursos que faltaban
sirviendo 200, aria-label emitido en el HTML, e interfaz intacta (10 categorias,
50 modulos, 7 accesos rapidos, 0 etiquetas vacias).
This commit is contained in:
SITO 2026-08-18 23:48:14 +02:00
parent 7459644a73
commit c3a1c841e6
27 changed files with 1994 additions and 220 deletions

View file

@ -738,6 +738,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel }
if (a.type === 'task' && String(c.isPublic || '').toUpperCase() === 'PRIVATE' && a.author !== userId && !(Array.isArray(c.assignees) && c.assignees.includes(userId))) return false;
if (a.type === 'forum' && c.isPrivate === true && a.author !== userId) return false;
if (a.type === 'job' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && a.author !== userId && !(Array.isArray(c.subscribers) && c.subscribers.includes(userId))) return false;
if (a.type === 'housing' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && a.author !== userId) return false;
if (a.type === 'market' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && c.seller !== userId) return false;
if (a.type === 'shop' && String(c.visibility || '').toUpperCase() === 'CLOSED' && a.author !== userId) return false;
if (a.type === 'curriculum' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && a.author !== userId) return false;

View file

@ -369,7 +369,7 @@ module.exports = ({ cooler }) => {
const isOwner = viewer === target;
const arr = (v) => Array.isArray(v) ? v : [];
const up = (v) => String(v || '').toUpperCase();
const COUNTED = new Set(['post','event','task','forum','tribe','market','job','project','industry','shop','image','video','audio','document','bookmark','transfer','map']);
const COUNTED = new Set(['post','event','task','forum','tribe','market','job','housing','project','industry','shop','image','video','audio','document','bookmark','transfer','map']);
const accessible = (type, c) => {
if (c.encryptedPayload) return false;
switch (type) {
@ -377,6 +377,7 @@ module.exports = ({ cooler }) => {
case 'event': return String(c.isPublic || '').toLowerCase() !== 'private' || isOwner || arr(c.attendees).includes(viewer);
case 'forum': return c.isPrivate !== true || isOwner;
case 'job': return up(c.visibility) !== 'HIDDEN' || isOwner || arr(c.subscribers).includes(viewer);
case 'housing': return up(c.visibility) !== 'HIDDEN' || isOwner;
case 'market': return up(c.visibility) !== 'HIDDEN' || isOwner;
case 'shop': return up(c.visibility) !== 'CLOSED' || isOwner;
case 'tribe': { const st = up(c.status); return !(st === 'PRIVATE' || st === 'INVITE-ONLY') || isOwner || arr(c.members).includes(viewer); }

View file

@ -79,7 +79,7 @@ module.exports = ({ cooler, tribeCrypto }) => {
const jobId = c.jobId
const k = `${jobId}::${author}`
const prev = jobSubLatest.get(k)
if (!prev || ts > prev.ts) jobSubLatest.set(k, { ts, value: !!c.value, author, jobId })
if (!prev || ts >= prev.ts) jobSubLatest.set(k, { ts, value: !!c.value, author, jobId })
continue
}
}
@ -134,7 +134,7 @@ module.exports = ({ cooler, tribeCrypto }) => {
const jobId = c.jobId
const k = `${jobId}::${author}`
const prev = jobSubLatest.get(k)
if (!prev || ts > prev.ts) jobSubLatest.set(k, { ts, value: !!c.value, author, jobId })
if (!prev || ts >= prev.ts) jobSubLatest.set(k, { ts, value: !!c.value, author, jobId })
} catch {}
}
}

View file

@ -165,6 +165,11 @@ function getGoverningHouseKey(now = new Date()) {
return HOUSE_KEYS[now.getMonth() % HOUSE_KEYS.length];
}
function getGoverningPeriodId(now = new Date()) {
const month = String(now.getMonth() + 1).padStart(2, '0');
return `${getGoverningHouseKey(now)}:${now.getFullYear()}-${month}`;
}
module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
let ssb;
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
@ -922,6 +927,7 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
PROFILE_QUESTIONS,
computeCycle,
getGoverningHouseKey,
getGoverningPeriodId,
publishJoin,
publishLeaveLarp,
getUserHouse,

View file

@ -23,7 +23,7 @@ module.exports = ({ cooler }) => {
const validTypes = [
'bookmark', 'votes', 'transfer',
'feed', 'image', 'audio', 'video', 'document', 'torrent',
'industry', 'project', 'report', 'task', 'event', 'shopProduct'
'industry', 'project', 'report', 'task', 'event', 'shopProduct', 'housing', 'market'
];
const getPreview = c => {
@ -46,7 +46,9 @@ module.exports = ({ cooler }) => {
report: 'reportOpinion',
task: 'taskOpinion',
event: 'eventOpinion',
shopProduct: 'shopOpinion'
shopProduct: 'shopOpinion',
housing: 'housingOpinion',
market: 'marketOpinion'
};
const createVote = async (contentId, category) => {

View file

@ -49,7 +49,7 @@ module.exports = ({ cooler }) => {
async sentRefs() {
const out = new Set();
let messages = [];
try { messages = await this.listAllPrivate(); } catch (_) { return out; }
try { messages = await this.listAllPrivate({ includeDeleted: true }); } catch (_) { return out; }
for (const m of (messages || [])) {
const c = m && m.value && m.value.content;
if (!c || !c.ref) continue;
@ -116,7 +116,8 @@ module.exports = ({ cooler }) => {
return publishAsync(tombstone, tombstoneRecps);
},
async listAllPrivate() {
async listAllPrivate(opts = {}) {
const includeDeleted = !!(opts && opts.includeDeleted);
const ssbClient = await openSsb();
const raw = await new Promise((resolve, reject) => {
pull(
@ -174,7 +175,7 @@ module.exports = ({ cooler }) => {
}
}
}
return posts.filter(m => m && m.key && !tombed.has(m.key));
return posts.filter(m => m && m.key && (includeDeleted || !tombed.has(m.key)));
}
};
};

View file

@ -239,6 +239,7 @@ module.exports = ({ cooler }) => {
if (goal < 0) goal = 0
const deadlineISO = data.deadline ? new Date(data.deadline).toISOString() : null
if (deadlineISO && new Date(deadlineISO).getTime() < Date.now()) throw new Error("The deadline cannot be in the past")
const content = {
type: TYPE,
@ -323,7 +324,11 @@ module.exports = ({ cooler }) => {
}
let deadline = patch.deadline === undefined ? current.deadline : patch.deadline
if (deadline != null && deadline !== "") deadline = new Date(deadline).toISOString()
if (deadline != null && deadline !== "") {
deadline = new Date(deadline).toISOString()
const changed = !current.deadline || new Date(current.deadline).getTime() !== new Date(deadline).getTime()
if (changed && new Date(deadline).getTime() < Date.now()) throw new Error("The deadline cannot be in the past")
}
else if (deadline === "") deadline = null
const updated = {

View file

@ -40,7 +40,7 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => {
'post', 'about', 'curriculum', 'tribe', 'transfer', 'feed',
'votes', 'report', 'task', 'event', 'bookmark', 'document',
'image', 'audio', 'video', 'torrent', 'market', 'bankWallet', 'bankClaim',
'project', 'job', 'industry', 'industryBlueprint', 'forum', 'vote', 'contact', 'pub', 'map', 'shop', 'shopProduct', 'chat', 'pad', 'poll'
'project', 'job', 'housing', 'industry', 'industryBlueprint', 'forum', 'vote', 'contact', 'pub', 'map', 'shop', 'shopProduct', 'chat', 'pad', 'poll'
];
const getRelevantFields = (type, content) => {

View file

@ -161,6 +161,7 @@ module.exports = ({ cooler, padsModel, tribesModel }) => {
if (c.type === 'task' && String(c.isPublic).toUpperCase() === 'PRIVATE') return false;
if ((c.type === 'chat' || c.type === 'pad' || c.type === 'map' || c.type === 'calendar') && c.status === 'INVITE-ONLY' && c.author !== viewerId && !(Array.isArray(c.members) && c.members.includes(viewerId))) return false;
if (c.type === 'shop' && c.visibility === 'CLOSED') return false;
if (c.type === 'housing' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && c.author !== viewerId) return false;
if (c.type === 'poll' && c.chatId) return false;
return true;
});

View file

@ -21,7 +21,7 @@ module.exports = ({ cooler }) => {
const types = [
'bookmark', 'votes', 'poll', 'feed',
'image', 'audio', 'video', 'document', 'torrent', 'transfer',
'industry', 'project', 'report', 'task', 'event', 'shopProduct'
'industry', 'project', 'report', 'task', 'event', 'shopProduct', 'housing', 'market'
];
const categories = opinionCategories;