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).
554 lines
28 KiB
JavaScript
554 lines
28 KiB
JavaScript
const { div, h2, h3, p, section, button, form, a, input, span, pre, table, tr, td, strong } = require("../server/node_modules/hyperaxe");
|
|
const { template, i18n, userLink } = require("../views/main_views");
|
|
const moment = require("../server/node_modules/moment");
|
|
|
|
const FILTER_LABELS = {
|
|
votes: i18n.typeVotes, vote: i18n.typeVote, recent: i18n.recent, all: i18n.all,
|
|
mine: i18n.mine, tombstone: i18n.typeTombstone, logs: i18n.typeLog || 'LOGS', pixelia: i18n.typePixelia,
|
|
curriculum: i18n.typeCurriculum, document: i18n.typeDocument, bookmark: i18n.typeBookmark,
|
|
feed: i18n.typeFeed, event: i18n.typeEvent, task: i18n.typeTask, report: i18n.typeReport,
|
|
image: i18n.typeImage, audio: i18n.typeAudio, video: i18n.typeVideo, post: i18n.typePost,
|
|
forum: i18n.typeForum, about: i18n.typeAbout, contact: i18n.typeContact, pub: i18n.typePub,
|
|
transfer: i18n.typeTransfer, market: i18n.typeMarket, job: i18n.typeJob, housing: i18n.typeHousing, tribe: i18n.typeTribe,
|
|
project: i18n.typeProject, industry: i18n.typeIndustry, industryBlueprint: i18n.industryBlueprints, banking: i18n.typeBanking, bankWallet: i18n.typeBankWallet, bankClaim: i18n.typeBankClaim,
|
|
aiExchange: i18n.typeAiExchange, parliament: i18n.typeParliament, courts: i18n.typeCourts,
|
|
map: i18n.typeMap, shop: i18n.typeShop, shopProduct: i18n.typeShopProduct || 'Shop Product',
|
|
pad: i18n.typePad || 'PAD', chat: i18n.typeChat || 'CHAT', gameScore: i18n.typeGameScore || 'GAME SCORE',
|
|
calendar: i18n.typeCalendar || 'CALENDAR', torrent: i18n.typeTorrent
|
|
};
|
|
|
|
const BASE_FILTERS = ['recent', 'all', 'mine', 'tombstone', 'logs'];
|
|
const CAT_BLOCK1 = ['votes', 'event', 'task', 'report', 'calendar', 'parliament', 'courts'];
|
|
const CAT_BLOCK2 = ['pub', 'tribe', 'about', 'contact', 'curriculum', 'vote', 'aiExchange'];
|
|
const CAT_BLOCK3 = ['banking', 'job', 'housing', 'market', 'project', 'industry', 'transfer', 'feed', 'post', 'pixelia', 'shop', 'gameScore'];
|
|
const CAT_BLOCK4 = ['forum', 'pad', 'chat', 'bookmark', 'image', 'video', 'audio', 'document', 'map', 'torrent'];
|
|
|
|
const SEARCH_FIELDS = ['author','id','from','to'];
|
|
|
|
const formatCarbon = (bytes) => {
|
|
const n = Number(bytes) || 0;
|
|
if (!n) return '0 µg CO₂';
|
|
const grams = (n / (1024 * 1024)) * 0.095;
|
|
if (grams >= 1) return `${grams.toFixed(2)} g CO₂`;
|
|
const mg = grams * 1000;
|
|
if (mg >= 1) return `${mg.toFixed(2)} mg CO₂`;
|
|
const ug = mg * 1000;
|
|
return `${ug.toFixed(2)} µg CO₂`;
|
|
};
|
|
|
|
const hiddenSearchInputs = (search) =>
|
|
SEARCH_FIELDS.map(k => {
|
|
const v = String(search?.[k] ?? '').trim();
|
|
return v ? input({ type: 'hidden', name: k, value: v }) : null;
|
|
}).filter(Boolean);
|
|
|
|
const toDatetimeLocal = (s) => {
|
|
const raw = String(s || '').trim();
|
|
if (!raw) return '';
|
|
const ts = new Date(raw).getTime();
|
|
if (!Number.isFinite(ts)) return '';
|
|
return moment(ts).format('YYYY-MM-DDTHH:mm');
|
|
};
|
|
|
|
const toQueryString = (filter, search = {}) => {
|
|
const parts = [];
|
|
const f = String(filter || '').trim();
|
|
if (f) parts.push(`filter=${encodeURIComponent(f)}`);
|
|
for (const k of SEARCH_FIELDS) {
|
|
const v = String(search?.[k] ?? '').trim();
|
|
if (v) parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(v)}`);
|
|
}
|
|
return parts.length ? `?${parts.join('&')}` : '';
|
|
};
|
|
|
|
const filterBlocks = (blocks, filter, userId) => {
|
|
if (filter === 'recent') return blocks.filter(b => Date.now() - b.ts < 24*60*60*1000);
|
|
if (filter === 'mine') return blocks.filter(b => b.author === userId);
|
|
if (filter === 'all') return blocks;
|
|
if (filter === 'banking') return blocks.filter(b => b.type === 'bankWallet' || b.type === 'bankClaim');
|
|
if (filter === 'parliament') {
|
|
const pset = new Set(['parliamentTerm','parliamentProposal','parliamentLaw','parliamentCandidature','parliamentRevocation']);
|
|
return blocks.filter(b => pset.has(b.type));
|
|
}
|
|
if (filter === 'courts') {
|
|
const cset = new Set(['courtsCase','courtsEvidence','courtsAnswer','courtsVerdict','courtsSettlement','courtsSettlementProposal','courtsSettlementAccepted','courtsNomination','courtsNominationVote']);
|
|
return blocks.filter(b => cset.has(b.type));
|
|
}
|
|
if (filter === 'shop') return blocks.filter(b => b.type === 'shop' || b.type === 'shopProduct');
|
|
if (filter === 'logs') return blocks.filter(b => b.type === 'log' && b.author === userId);
|
|
return blocks.filter(b => b.type === filter);
|
|
};
|
|
|
|
const computeStats = (blocks) => {
|
|
const arr = Array.isArray(blocks) ? blocks : [];
|
|
const byType = new Map();
|
|
const byAuthor = new Map();
|
|
for (const b of arr) {
|
|
if (!b || !b.type) continue;
|
|
byType.set(b.type, (byType.get(b.type) || 0) + 1);
|
|
if (b.author) byAuthor.set(b.author, (byAuthor.get(b.author) || 0) + 1);
|
|
}
|
|
const typeBreakdown = Array.from(byType.entries())
|
|
.map(([type, count]) => ({ type, count }))
|
|
.sort((a, b) => b.count - a.count);
|
|
const topAuthors = Array.from(byAuthor.entries())
|
|
.map(([author, count]) => ({ author, count }))
|
|
.sort((a, b) => b.count - a.count)
|
|
.slice(0, 5);
|
|
return { total: arr.length, typeBreakdown, topAuthors };
|
|
};
|
|
|
|
const renderStatsPanel = (stats, currentFilter, search) => {
|
|
if (!stats || stats.total === 0) return null;
|
|
const topTypes = stats.typeBreakdown.filter(t => t.type !== 'industryBlueprint').slice(0, 10);
|
|
return div({ class: 'blockchain-stats' },
|
|
div({ class: 'tags-header' },
|
|
h3(`${i18n.blockchainStatsTitle || 'Stats'} (${stats.total})`)
|
|
),
|
|
topTypes.length
|
|
? div({ class: 'blockchain-stats-types' },
|
|
h3({ class: 'blockchain-stats-subtitle' }, i18n.blockchainStatsTypeBreakdown || 'Type breakdown'),
|
|
div({ class: 'mode-buttons-cols' },
|
|
topTypes.map(({ type, count }) =>
|
|
form({ method: 'GET', action: '/blockexplorer' },
|
|
input({ type: 'hidden', name: 'filter', value: type }),
|
|
...hiddenSearchInputs(search),
|
|
button({
|
|
type: 'submit',
|
|
class: currentFilter === type ? 'filter-btn active' : 'filter-btn'
|
|
}, `${(FILTER_LABELS[type] || type).toUpperCase()} (${count})`)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
: null,
|
|
stats.topAuthors.length
|
|
? div({ class: 'blockchain-stats-authors' },
|
|
h3({ class: 'blockchain-stats-subtitle' }, i18n.blockchainStatsTopAuthors || 'Top authors'),
|
|
table({ class: 'block-info-table' },
|
|
tr(
|
|
td({ class: 'card-label' }, i18n.blockchainBlockAuthor),
|
|
td({ class: 'card-label' }, i18n.blockchainStatsCount || 'Blocks')
|
|
),
|
|
...stats.topAuthors.map(({ author, count }) =>
|
|
tr(
|
|
td(a({ href: `/blockexplorer?filter=all&author=${encodeURIComponent(author)}`, class: 'user-link block-author' }, author)),
|
|
td(String(count))
|
|
)
|
|
)
|
|
)
|
|
)
|
|
: null
|
|
);
|
|
};
|
|
|
|
const generateFilterButtons = (filters, currentFilter, action, search = {}) =>
|
|
div({ class: 'mode-buttons-cols' },
|
|
filters.map(mode =>
|
|
form({ method: 'GET', action },
|
|
input({ type: 'hidden', name: 'filter', value: mode }),
|
|
...hiddenSearchInputs(search),
|
|
button({
|
|
type: 'submit',
|
|
class: currentFilter === mode ? 'filter-btn active' : 'filter-btn'
|
|
}, (FILTER_LABELS[mode]||mode).toUpperCase())
|
|
)
|
|
)
|
|
);
|
|
|
|
const getViewDetailsAction = (type, block) => {
|
|
if (block && block.content && typeof block.content.encryptedPayload === 'string') return null;
|
|
switch (type) {
|
|
case 'votes': return `/votes/${encodeURIComponent(block.id)}`;
|
|
case 'transfer': return `/transfers/${encodeURIComponent(block.id)}`;
|
|
case 'pixelia': return `/pixelia`;
|
|
case 'tribe': return `/tribe/${encodeURIComponent(block.id)}`;
|
|
case 'curriculum': return `/inhabitant/${encodeURIComponent(block.author)}`;
|
|
case 'image': return `/images/${encodeURIComponent(block.id)}`;
|
|
case 'audio': return `/audios/${encodeURIComponent(block.id)}`;
|
|
case 'video': return `/videos/${encodeURIComponent(block.id)}`;
|
|
case 'forum': return `/forum/${encodeURIComponent(block.content?.key||block.id)}`;
|
|
case 'document': return `/documents/${encodeURIComponent(block.id)}`;
|
|
case 'bookmark': return `/bookmarks/${encodeURIComponent(block.id)}`;
|
|
case 'event': return `/events/${encodeURIComponent(block.id)}`;
|
|
case 'task': return `/tasks/${encodeURIComponent(block.id)}`;
|
|
case 'about': return `/author/${encodeURIComponent(block.author)}`;
|
|
case 'post': return `/thread/${encodeURIComponent(block.id)}#${encodeURIComponent(block.id)}`;
|
|
case 'vote': return `/thread/${encodeURIComponent(block.content.vote.link)}#${encodeURIComponent(block.content.vote.link)}`;
|
|
case 'contact': return `/inhabitants`;
|
|
case 'pub': return `/invites`;
|
|
case 'market': return `/market/${encodeURIComponent(block.id)}`;
|
|
case 'job': return `/jobs/${encodeURIComponent(block.id)}`;
|
|
case 'housing': return `/housing/${encodeURIComponent(block.id)}`;
|
|
case 'project': return `/projects/${encodeURIComponent(block.id)}`;
|
|
case 'industry': return `/industry/${encodeURIComponent(block.id)}`;
|
|
case 'industryBlueprint': return `/industry/blueprint/${encodeURIComponent(block.id)}`;
|
|
case 'report': return `/reports/${encodeURIComponent(block.id)}`;
|
|
case 'calendar': return `/calendars/${encodeURIComponent(block.id)}`;
|
|
case 'bankWallet': return `/wallet`;
|
|
case 'bankClaim': return `/banking${block.content?.epochId ? `/epoch/${encodeURIComponent(block.content.epochId)}` : ''}`;
|
|
case 'parliamentTerm': return `/parliament`;
|
|
case 'parliamentProposal': return `/parliament`;
|
|
case 'parliamentLaw': return `/parliament`;
|
|
case 'parliamentCandidature': return `/parliament`;
|
|
case 'parliamentRevocation': return `/parliament`;
|
|
case 'courtsCase': return `/courts`;
|
|
case 'courtsEvidence': return `/courts`;
|
|
case 'courtsAnswer': return `/courts`;
|
|
case 'courtsVerdict': return `/courts`;
|
|
case 'courtsSettlement': return `/courts`;
|
|
case 'courtsSettlementProposal': return `/courts`;
|
|
case 'courtsSettlementAccepted': return `/courts`;
|
|
case 'courtsNomination': return `/courts`;
|
|
case 'courtsNominationVote': return `/courts`;
|
|
case 'map': return `/maps/${encodeURIComponent(block.id)}`;
|
|
case 'torrent': return `/torrents/${encodeURIComponent(block.id)}`;
|
|
case 'mapMarker': return block.content?.mapId ? `/maps/${encodeURIComponent(block.content.mapId)}` : `/maps`;
|
|
case 'shop': return `/shops/${encodeURIComponent(block.id)}`;
|
|
case 'shopProduct': return `/shops/product/${encodeURIComponent(block.id)}`;
|
|
case 'pad': return `/pads/${encodeURIComponent(block.id)}`;
|
|
case 'chat': return `/chats/${encodeURIComponent(block.id)}`;
|
|
case 'gameScore': return `/games?filter=scoring`;
|
|
case 'log': return `/logs/view/${encodeURIComponent(block.id)}`;
|
|
case 'poll': return `/polls/${encodeURIComponent(block.id)}`;
|
|
case 'calendarDate':
|
|
case 'calendarNote': return block.content?.calendarId ? `/calendars/${encodeURIComponent(block.content.calendarId)}` : `/calendars`;
|
|
case 'padEntry': return block.content?.padId ? `/pads/${encodeURIComponent(block.content.padId)}` : `/pads`;
|
|
case 'chatMessage': return block.content?.roomId ? `/chats/${encodeURIComponent(block.content.roomId)}` : `/chats`;
|
|
default: return null;
|
|
}
|
|
};
|
|
|
|
const TYPE_COLORS = {
|
|
post:'#3498db', vote:'#9b59b6', votes:'#9b59b6', about:'#1abc9c', contact:'#16a085',
|
|
pub:'#2ecc71', tribe:'#e67e22', event:'#e74c3c', task:'#f39c12', report:'#c0392b',
|
|
image:'#2980b9', audio:'#8e44ad', video:'#d35400', document:'#27ae60', bookmark:'#f1c40f',
|
|
forum:'#1abc9c', feed:'#95a5a6', transfer:'#e74c3c', market:'#e67e22', job:'#3498db', housing:'#16a085',
|
|
project:'#2ecc71', banking:'#f39c12', bankWallet:'#f39c12', bankClaim:'#f39c12',
|
|
pixelia:'#9b59b6', curriculum:'#1abc9c', aiExchange:'#3498db', tombstone:'#7f8c8d',
|
|
parliamentTerm:'#8e44ad', parliamentProposal:'#8e44ad', parliamentLaw:'#8e44ad',
|
|
parliamentCandidature:'#8e44ad', parliamentRevocation:'#8e44ad',
|
|
courtsCase:'#c0392b', courtsEvidence:'#c0392b', courtsAnswer:'#c0392b',
|
|
courtsVerdict:'#c0392b', courtsSettlement:'#c0392b', courtsNomination:'#c0392b',
|
|
map:'#27ae60', mapMarker:'#27ae60',
|
|
shop:'#e67e22', shopProduct:'#e67e22',
|
|
pad:'#2ecc71', chat:'#3498db', gameScore:'#f39c12',
|
|
calendar:'#e74c3c'
|
|
};
|
|
|
|
const renderBlockDiagram = (blocks, qs) => {
|
|
const last2 = blocks.slice(0, 2);
|
|
if (!last2.length) return null;
|
|
|
|
return div({ class: 'block-diagram-section' },
|
|
h3({ class: 'block-diagram-title' }, i18n.blockchainLatestDatagram || 'Latest Datagram'),
|
|
...last2.map(block => {
|
|
const ts = moment(block.ts).format('YYYY-MM-DD HH:mm:ss');
|
|
const typeLabel = (FILTER_LABELS[block.type] || block.type).toUpperCase();
|
|
const color = TYPE_COLORS[block.type] || '#95a5a6';
|
|
const shortId = block.id.length > 20 ? block.id.slice(0, 10) + '…' + block.id.slice(-8) : block.id;
|
|
const shortAuthor = block.author.length > 20 ? block.author.slice(0, 10) + '…' + block.author.slice(-8) : block.author;
|
|
const contentKeys = Object.keys(block.content || {}).filter(k => k !== 'type').join(', ');
|
|
const flags = [
|
|
block.isTombstoned ? 'TOMBSTONED' : null,
|
|
block.isReplaced ? 'REPLACED' : null,
|
|
block.content?.replaces ? 'EDIT' : null
|
|
].filter(Boolean).join(' | ') || '—';
|
|
|
|
const datagramQs = qs ? `${qs}&view=datagram` : '?view=datagram';
|
|
const typeClass = `bd-type-${String(block.type || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '-')}`;
|
|
return a({ href: `/blockexplorer/block/${encodeURIComponent(block.id)}${datagramQs}`, class: 'block-diagram-link' },
|
|
div({ class: `block-diagram ${typeClass}` },
|
|
div({ class: 'block-diagram-ruler' },
|
|
span('0'), span('4'), span('8'), span('16'), span('24'), span('31')
|
|
),
|
|
div({ class: 'block-diagram-grid' },
|
|
div({ class: 'block-diagram-cell bd-seq' },
|
|
span({ class: 'bd-label' }, 'SEQ:'),
|
|
span({ class: 'bd-value' }, String(block.content?.sequence || '—'))
|
|
),
|
|
div({ class: 'block-diagram-cell bd-type' },
|
|
span({ class: 'bd-label' }, 'TYPE:'),
|
|
span({ class: 'bd-value' }, typeLabel)
|
|
),
|
|
div({ class: 'block-diagram-cell bd-ts' },
|
|
span({ class: 'bd-label' }, 'TIMESTAMP:'),
|
|
span({ class: 'bd-value' }, ts)
|
|
),
|
|
div({ class: 'block-diagram-cell bd-id' },
|
|
span({ class: 'bd-label' }, 'BLOCK ID:'),
|
|
span({ class: 'bd-value' }, shortId)
|
|
),
|
|
div({ class: 'block-diagram-cell bd-author' },
|
|
span({ class: 'bd-label' }, 'AUTHOR:'),
|
|
span({ class: 'bd-value' }, shortAuthor)
|
|
),
|
|
div({ class: 'block-diagram-cell bd-flags' },
|
|
span({ class: 'bd-label' }, 'FLAGS:'),
|
|
span({ class: 'bd-value' }, flags)
|
|
),
|
|
div({ class: 'block-diagram-cell bd-ctype' },
|
|
span({ class: 'bd-label' }, 'CONTENT.TYPE:'),
|
|
span({ class: 'bd-value' }, block.content?.type || '—')
|
|
),
|
|
div({ class: 'block-diagram-cell bd-data' },
|
|
span({ class: 'bd-label' }, 'CONTENT:'),
|
|
span({ class: 'bd-value' }, contentKeys || '—')
|
|
)
|
|
)
|
|
)
|
|
);
|
|
})
|
|
);
|
|
};
|
|
|
|
const renderSingleBlockView = (block, filter = 'recent', userId, search = {}, viewMode = 'block', restricted = false) => {
|
|
if (!block || block.notAvailable) {
|
|
return template(
|
|
i18n.blockchain,
|
|
section(
|
|
div({ class: 'tags-header' },
|
|
h2(i18n.blockchain),
|
|
p(i18n.blockchainDescription)
|
|
),
|
|
div({ class: 'block-single' },
|
|
block && block.id ? div({ class: 'block-row block-row--meta' },
|
|
span({ class: 'blockchain-card-label' }, `${i18n.blockchainBlockID || 'Block ID'}:`),
|
|
span({ class: 'blockchain-card-value' }, block.id)
|
|
) : null,
|
|
p({ class: 'empty' }, i18n.blockchainBlockNotAvailable || 'This block is not available in your local feed. It may belong to a peer you are not yet replicating from.'),
|
|
div({ class: 'larp-actions' },
|
|
a({ href: '/blockexplorer', class: 'filter-btn' }, i18n.blockchainBackToBlockexplorer || 'Back to blockexplorer')
|
|
)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
const qs = toQueryString(filter, search);
|
|
const isDatagram = viewMode === 'datagram';
|
|
|
|
const blockContent = restricted
|
|
? div(
|
|
div({ class: 'block-single' },
|
|
div({ class: 'block-row block-row--meta' },
|
|
span({ class: 'blockchain-card-label' }, `${i18n.blockchainBlockID}:`),
|
|
span({ class: 'blockchain-card-value' }, block.id)
|
|
),
|
|
div({ class: 'block-row block-row--meta' },
|
|
span({ class: 'blockchain-card-label' }, `${i18n.blockchainBlockType}:`),
|
|
span({ class: 'blockchain-card-value' }, (FILTER_LABELS[block.type]||block.type).toUpperCase())
|
|
),
|
|
div({ class: 'block-row block-row--meta' },
|
|
span({ class: 'blockchain-card-label' }, `${i18n.blockchainBlockTimestamp}:`),
|
|
span({ class: 'blockchain-card-value' }, moment(block.ts).format('YYYY-MM-DDTHH:mm:ss.SSSZ'))
|
|
)
|
|
),
|
|
div({ class: 'block-row block-row--content' },
|
|
p({ class: 'access-denied-msg' }, i18n.blockAccessRestricted)
|
|
)
|
|
)
|
|
: isDatagram
|
|
? renderBlockDiagram([block], qs)
|
|
: div(
|
|
div({ class: 'block-single' },
|
|
div({ class: 'block-row block-row--meta' },
|
|
span({ class: 'blockchain-card-label' }, `${i18n.blockchainBlockID}:`),
|
|
span({ class: 'blockchain-card-value' }, block.id)
|
|
),
|
|
div({ class: 'block-row block-row--meta' },
|
|
span({ class: 'blockchain-card-label' }, `${i18n.blockchainBlockType}:`),
|
|
span({ class: 'blockchain-card-value' }, (FILTER_LABELS[block.type]||block.type).toUpperCase())
|
|
),
|
|
div({ class: 'block-row block-row--meta' },
|
|
span({ class: 'blockchain-card-label' }, `${i18n.oasisIdLabel || 'OasisID'}:`),
|
|
a({ href:`/author/${encodeURIComponent(block.author)}`, class:'block-author user-link' }, block.author)
|
|
)
|
|
),
|
|
div({ class:'block-row block-row--content' },
|
|
div({ class:'block-content-preview' },
|
|
block.content && typeof block.content.encryptedPayload === 'string'
|
|
? (() => {
|
|
const { renderEncryptedChip } = require('./clearnet_view');
|
|
return div({ class: 'encrypted-payload-box' },
|
|
div({ class: 'title-with-chip' }, renderEncryptedChip(i18n)),
|
|
pre({ class: 'json-content' }, String(block.content.encryptedPayload).slice(0, 128) + (String(block.content.encryptedPayload).length > 128 ? '…' : ''))
|
|
);
|
|
})()
|
|
: pre({ class:'json-content' }, JSON.stringify(block.content,null,2))
|
|
)
|
|
),
|
|
div({ class: 'block-single' },
|
|
div({ class: 'block-row block-row--meta' },
|
|
span({ class: 'blockchain-card-label' }, `${i18n.blockchainBlockTimestamp}:`),
|
|
span({ class: 'blockchain-card-value' }, moment(block.ts).format('YYYY-MM-DDTHH:mm:ss.SSSZ'))
|
|
),
|
|
div({ class: 'block-row block-row--meta' },
|
|
span({ class: 'blockchain-card-label' }, `${i18n.blockchainBlockCarbon || 'Carbon footprint'}:`),
|
|
span({ class: 'blockchain-card-value' }, formatCarbon(block.size))
|
|
)
|
|
)
|
|
);
|
|
|
|
return template(
|
|
i18n.blockchain,
|
|
section(
|
|
div({ class: 'tags-header' },
|
|
h2(i18n.blockchain),
|
|
p(i18n.blockchainDescription)
|
|
),
|
|
div({ class: 'mode-buttons-row' },
|
|
div({ class: 'filter-column' },
|
|
generateFilterButtons(BASE_FILTERS, filter, '/blockexplorer', search)
|
|
),
|
|
div({ class: 'filter-column' },
|
|
generateFilterButtons(CAT_BLOCK1, filter, '/blockexplorer', search),
|
|
generateFilterButtons(CAT_BLOCK2, filter, '/blockexplorer', search)
|
|
),
|
|
div({ class: 'filter-column' },
|
|
generateFilterButtons(CAT_BLOCK3, filter, '/blockexplorer', search),
|
|
generateFilterButtons(CAT_BLOCK4, filter, '/blockexplorer', search)
|
|
)
|
|
),
|
|
blockContent,
|
|
div({ class:'block-row block-row--back' },
|
|
form({ method:'GET', action:'/blockexplorer' },
|
|
input({ type: 'hidden', name: 'filter', value: filter }),
|
|
...hiddenSearchInputs(search),
|
|
button({ type:'submit', class:'filter-btn' }, `← ${i18n.blockchainBack}`)
|
|
),
|
|
!block.isTombstoned && !block.isReplaced && getViewDetailsAction(block.type, block) ?
|
|
a({ href:getViewDetailsAction(block.type, block), class:'btn-singleview btn-content', title:i18n.visitContent }, '↗')
|
|
: (block.isTombstoned || block.isReplaced) ?
|
|
div({ class: 'deleted-label' },
|
|
i18n.blockchainContentDeleted || "This content has been deleted."
|
|
)
|
|
: null
|
|
)
|
|
)
|
|
);
|
|
};
|
|
|
|
const renderBlockchainView = (blocks, filter, userId, search = {}, extras = {}) => {
|
|
const s = search || {};
|
|
const authorVal = String(s.author || '');
|
|
const idVal = String(s.id || '');
|
|
const fromVal = toDatetimeLocal(s.from);
|
|
const toVal = toDatetimeLocal(s.to);
|
|
const inspect = extras && extras.inspect ? extras.inspect : null;
|
|
const inspectVal = inspect ? String(inspect.block || '') : '';
|
|
|
|
const shown = filterBlocks(blocks, filter, userId);
|
|
const qs = toQueryString(filter, s);
|
|
|
|
return template(
|
|
i18n.blockchain,
|
|
section(
|
|
div({ class:'tags-header' },
|
|
h2(i18n.blockchain),
|
|
p(i18n.blockchainDescription)
|
|
),
|
|
div({ class:'mode-buttons-row' },
|
|
div({ class: 'filter-column' },
|
|
generateFilterButtons(BASE_FILTERS, filter, '/blockexplorer', s)
|
|
),
|
|
div({ class: 'filter-column' },
|
|
generateFilterButtons(CAT_BLOCK1, filter, '/blockexplorer', s),
|
|
generateFilterButtons(CAT_BLOCK2, filter, '/blockexplorer', s)
|
|
),
|
|
div({ class: 'filter-column' },
|
|
generateFilterButtons(CAT_BLOCK3, filter, '/blockexplorer', s),
|
|
generateFilterButtons(CAT_BLOCK4, filter, '/blockexplorer', s)
|
|
)
|
|
),
|
|
div({ class: 'blockexplorer-search' },
|
|
form({ method: 'GET', action: '/blockexplorer', class: 'blockexplorer-search-form' },
|
|
input({ type: 'hidden', name: 'filter', value: filter }),
|
|
div({ class: 'blockexplorer-search-row' },
|
|
div({ class: 'blockexplorer-search-pair' },
|
|
input({ type: 'text', name: 'id', value: idVal, placeholder: i18n.blockchainBlockID, class: 'blockexplorer-search-input' }),
|
|
input({ type: 'text', name: 'author', value: authorVal, placeholder: i18n.courtsJudgeIdPh, class: 'blockexplorer-search-input' })
|
|
),
|
|
div({ class: 'blockexplorer-search-dates' },
|
|
input({ type: 'datetime-local', name: 'from', value: fromVal, class: 'blockexplorer-search-input' }),
|
|
input({ type: 'datetime-local', name: 'to', value: toVal, class: 'blockexplorer-search-input' })
|
|
),
|
|
div({ class: 'blockexplorer-search-actions' },
|
|
div({ class: 'filter-box__controls' }, button({ type: 'submit', class: 'filter-box__button' }, i18n.searchSubmit))
|
|
)
|
|
)
|
|
)
|
|
),
|
|
div({ id: 'inspect', class: 'blockexplorer-inspect' },
|
|
h3(i18n.bankTaxesLookupTitle || 'Inspect a block'),
|
|
p(i18n.bankTaxesLookupNote || 'Enter a block ID to look it up in your local feed. The inspector shows the block ID (linked to its detail page), its author, its content type, its size in bytes, its carbon footprint and its ECO tax in ECO.'),
|
|
form({ method: 'GET', action: '/blockexplorer', class: 'blockexplorer-search-form' },
|
|
input({ type: 'hidden', name: 'filter', value: filter }),
|
|
div({ class: 'blockexplorer-search-row' },
|
|
div({ class: 'blockexplorer-search-pair' },
|
|
input({ type: 'text', name: 'inspect', value: inspectVal, placeholder: i18n.bankTaxesLookupPlaceholder || 'Block ID (e.g. %abc...=.sha256)', class: 'blockexplorer-search-input' })
|
|
),
|
|
div({ class: 'blockexplorer-search-actions' },
|
|
button({ type: 'submit', class: 'filter-box__button' }, i18n.bankTaxesLookupButton || 'Inspect')
|
|
)
|
|
)
|
|
),
|
|
inspect
|
|
? (inspect.found
|
|
? div({ class: 'bank-taxes-lookup-result' },
|
|
table({ class: 'bank-info-table' },
|
|
tr(td({ class: 'card-label' }, i18n.blockchainBlockID || 'Block ID'), td({ class: 'card-value' }, a({ href: `/blockexplorer/block/${encodeURIComponent(inspect.block)}` }, inspect.block))),
|
|
tr(td({ class: 'card-label' }, i18n.bankTaxesLookupAuthor || 'Author'), td({ class: 'card-value' }, inspect.author ? userLink(inspect.author) : '—')),
|
|
tr(td({ class: 'card-label' }, i18n.bankTaxesLookupType || 'Type'), td({ class: 'card-value' }, String(inspect.blockType || '—'))),
|
|
tr(td({ class: 'card-label' }, i18n.bankTaxesLookupSize || 'Size'), td({ class: 'card-value' }, `${Number(inspect.size || 0).toLocaleString()} B`)),
|
|
tr(td({ class: 'card-label' }, i18n.bankTaxesLookupCarbon || 'Carbon footprint'), td({ class: 'card-value' }, formatCarbon(inspect.size || 0))),
|
|
tr(td({ class: 'card-label' }, i18n.bankTaxesLookupEcoin || 'ECO Tax'), td({ class: 'card-value' }, `${Number(inspect.ecoinTax || 0).toFixed(6)} ECO`))
|
|
)
|
|
)
|
|
: p({ class: 'empty' }, i18n.bankTaxesLookupNotFound || 'No block found in your local feed with that ID.'))
|
|
: null
|
|
),
|
|
renderBlockDiagram(shown, qs),
|
|
h2({ class: 'block-diagram-title' }, 'Blockchain Blocks'),
|
|
shown.length === 0
|
|
? div(p(i18n.blockchainNoBlocks))
|
|
: shown
|
|
.sort((a,b)=>{
|
|
const ta = a.type==='market'&&a.content.updatedAt
|
|
? new Date(a.content.updatedAt).getTime()
|
|
: a.ts;
|
|
const tb = b.type==='market'&&b.content.updatedAt
|
|
? new Date(b.content.updatedAt).getTime()
|
|
: b.ts;
|
|
return tb - ta;
|
|
})
|
|
.map(block=>
|
|
div({ class:'block' },
|
|
div({ class:'block-buttons' },
|
|
block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}`, class:'btn-singleview', title:i18n.blockchainDetails }, '⦿'),
|
|
block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}&view=datagram`, class:'btn-singleview btn-datagram', title:i18n.blockchainDatagram }, '⊞'),
|
|
!block.isTombstoned && !block.isReplaced && getViewDetailsAction(block.type, block) ?
|
|
a({ href:getViewDetailsAction(block.type, block), class:'btn-singleview btn-content', title:i18n.visitContent }, '↗')
|
|
: (block.isTombstoned || block.isReplaced) ?
|
|
div({ class: 'deleted-label' },
|
|
i18n.blockchainContentDeleted || "This content has been deleted."
|
|
)
|
|
: null
|
|
),
|
|
div({ class:'block-row block-row--meta' },
|
|
table({ class:'block-info-table' },
|
|
tr(td({ class:'card-label' }, i18n.blockchainBlockTimestamp), td({ class:'card-value' }, moment(block.ts).format('YYYY-MM-DDTHH:mm:ss.SSSZ'))),
|
|
tr(td({ class:'card-label' }, i18n.blockchainBlockID), td({ class:'card-value' }, block.id)),
|
|
tr(td({ class:'card-label' }, i18n.blockchainBlockType), td({ class:'card-value' }, (FILTER_LABELS[block.type]||block.type).toUpperCase())),
|
|
tr(td({ class:'card-label' }, i18n.blockchainBlockAuthor), td({ class:'card-value' }, a({ href:`/author/${encodeURIComponent(block.author)}`, class:'block-author user-link' }, block.author))),
|
|
tr(td({ class:'card-label' }, i18n.blockchainBlockCarbon || 'Carbon footprint'), td({ class:'card-value' }, formatCarbon(block.size)))
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
);
|
|
};
|
|
|
|
module.exports = { renderBlockchainView, renderSingleBlockView, computeStats };
|
|
|