Browse Source

Oasis release 0.5.5

psy 1 day ago
parent
commit
578606471b

+ 7 - 0
README.md

@@ -64,6 +64,7 @@ Oasis is TRULY MODULAR. Here's a list of what comes deployed with the "core".
  + BlockExplorer: Module to navigate the blockchain.
  + BlockExplorer: Module to navigate the blockchain.
  + Bookmarks: Module to discover and manage bookmarks.	
  + Bookmarks: Module to discover and manage bookmarks.	
  + Cipher: Module to encrypt and decrypt your text symmetrically (using a shared password).	
  + Cipher: Module to encrypt and decrypt your text symmetrically (using a shared password).	
+ + Courts: Module to resolve conflicts and emit veredicts.	
  + Documents: Module to discover and manage documents.	
  + Documents: Module to discover and manage documents.	
  + Events: Module to discover and manage events.	
  + Events: Module to discover and manage events.	
  + Feed: Module to discover and share short-texts (feeds).
  + Feed: Module to discover and share short-texts (feeds).
@@ -117,6 +118,12 @@ Oasis contains its own Parliament (Government system).
 
 
   ![SNH](https://solarnethub.com/git/oasis-parliament.png "SolarNET.HuB")
   ![SNH](https://solarnethub.com/git/oasis-parliament.png "SolarNET.HuB")
   
   
+## Courts (justice)
+
+Oasis contains its own Courts (Justice system).
+
+  ![SNH](https://solarnethub.com/git/oasis-courts.png "SolarNET.HuB")
+  
 ## ECOin (crypto-economy)
 ## ECOin (crypto-economy)
 
 
 Oasis contains its own cryptocurrency. With it, you can exchange items and services in the marketplace. 
 Oasis contains its own cryptocurrency. With it, you can exchange items and services in the marketplace. 

+ 6 - 0
docs/CHANGELOG.md

@@ -13,6 +13,12 @@ All notable changes to this project will be documented in this file.
 ### Security
 ### Security
 -->
 -->
 
 
+## v0.5.5 - 2025-11-15
+
+### Added
+
+ + Conflicts resolution system (Courts plugin).
+ 
 ## v0.5.4 - 2025-10-30
 ## v0.5.4 - 2025-10-30
 
 
 ### Fixed
 ### Fixed

+ 454 - 5
src/backend/backend.js

@@ -299,9 +299,9 @@ const forumModel = require('../models/forum_model')({ cooler, isPublic: config.p
 const blockchainModel = require('../models/blockchain_model')({ cooler, isPublic: config.public });
 const blockchainModel = require('../models/blockchain_model')({ cooler, isPublic: config.public });
 const jobsModel = require('../models/jobs_model')({ cooler, isPublic: config.public });
 const jobsModel = require('../models/jobs_model')({ cooler, isPublic: config.public });
 const projectsModel = require("../models/projects_model")({ cooler, isPublic: config.public });
 const projectsModel = require("../models/projects_model")({ cooler, isPublic: config.public });
-const bankingModel = require("../models/banking_model")({ services: { cooler }, isPublic: config.public })
-const parliamentModel = require('../models/parliament_model')({ cooler, services: { tribes: tribesModel, votes: votesModel, inhabitants: inhabitantsModel, banking: bankingModel }
-});
+const bankingModel = require("../models/banking_model")({ services: { cooler }, isPublic: config.public });
+const parliamentModel = require('../models/parliament_model')({ cooler, services: { tribes: tribesModel, votes: votesModel, inhabitants: inhabitantsModel, banking: bankingModel } });
+const courtsModel = require('../models/courts_model')({ cooler, services: { votes: votesModel, inhabitants: inhabitantsModel, tribes: tribesModel, banking: bankingModel } });
 
 
 // starting warmup
 // starting warmup
 about._startNameWarmup();
 about._startNameWarmup();
@@ -546,6 +546,7 @@ const { jobsView, singleJobsView, renderJobForm } = require("../views/jobs_view"
 const { projectsView, singleProjectView } = require("../views/projects_view")
 const { projectsView, singleProjectView } = require("../views/projects_view")
 const { renderBankingView, renderSingleAllocationView, renderEpochView } = require("../views/banking_views")
 const { renderBankingView, renderSingleAllocationView, renderEpochView } = require("../views/banking_views")
 const { parliamentView } = require("../views/parliament_view");
 const { parliamentView } = require("../views/parliament_view");
+const { courtsView, courtsCaseView } = require('../views/courts_view');
 
 
 let sharp;
 let sharp;
 
 
@@ -672,7 +673,7 @@ router
     'popular', 'topics', 'summaries', 'latest', 'threads', 'multiverse', 'invites', 'wallet', 
     'popular', 'topics', 'summaries', 'latest', 'threads', 'multiverse', 'invites', 'wallet', 
     'legacy', 'cipher', 'bookmarks', 'videos', 'docs', 'audios', 'tags', 'images', 'trending', 
     'legacy', 'cipher', 'bookmarks', 'videos', 'docs', 'audios', 'tags', 'images', 'trending', 
     'events', 'tasks', 'market', 'tribes', 'votes', 'reports', 'opinions', 'transfers', 
     'events', 'tasks', 'market', 'tribes', 'votes', 'reports', 'opinions', 'transfers', 
-    'feed', 'pixelia', 'agenda', 'ai', 'forum', 'jobs', 'projects', 'banking', 'parliament'
+    'feed', 'pixelia', 'agenda', 'ai', 'forum', 'jobs', 'projects', 'banking', 'parliament', 'courts'
     ];
     ];
     const moduleStates = modules.reduce((acc, mod) => {
     const moduleStates = modules.reduce((acc, mod) => {
       acc[`${mod}Mod`] = configMods[`${mod}Mod`];
       acc[`${mod}Mod`] = configMods[`${mod}Mod`];
@@ -1108,6 +1109,155 @@ router
       revocationsEnactedCount 
       revocationsEnactedCount 
     });
     });
   })
   })
+  .get('/courts', async (ctx) => {
+    const mod = ctx.cookies.get('courtsMod') || 'on';
+    if (mod !== 'on') {
+      ctx.redirect('/modules');
+      return;
+    }
+    const filter = String(ctx.query.filter || 'cases').toLowerCase();
+    const search = String(ctx.query.search || '').trim();
+    const state = {
+      filter,
+      search,
+      cases: [],
+      myCases: [],
+      trials: [],
+      history: [],
+      nominations: [],
+      userId: null
+    };
+    const currentUserId = await courtsModel.getCurrentUserId();
+    state.userId = currentUserId;
+    if (filter === 'cases') {
+      let allCases = await courtsModel.listCases('open');
+      allCases = allCases.map((c) => ({
+        ...c,
+        respondent: c.respondentId || c.respondent
+      }));
+      if (search) {
+        const sLower = search.toLowerCase();
+        allCases = allCases.filter((c) => {
+          const t = String(c.title || '').toLowerCase();
+          const d = String(c.description || '').toLowerCase();
+          return t.includes(sLower) || d.includes(sLower);
+        });
+      }
+      state.cases = allCases;
+    }
+    if (filter === 'mycases' || filter === 'actions') {
+      let myCases = await courtsModel.listCasesForUser(currentUserId);
+      if (search) {
+        const sLower = search.toLowerCase();
+        myCases = myCases.filter((c) => {
+          const t = String(c.title || '').toLowerCase();
+          const d = String(c.description || '').toLowerCase();
+          return t.includes(sLower) || d.includes(sLower);
+        });
+      }
+      if (filter === 'actions') {
+        myCases = myCases.filter((c) => {
+          const status = String(c.status || '').toUpperCase();
+          const method = String(c.method || '').toUpperCase();
+          const isAccuser = !!c.isAccuser;
+          const isRespondent = !!c.isRespondent;
+          const isMediator = !!c.isMediator;
+          const isJudge = !!c.isJudge;
+          const isDictator = !!c.isDictator;
+          const canAnswer =
+            isRespondent && (status === 'OPEN' || status === 'IN_PROGRESS');
+          const canAssignJudge =
+            method === 'JUDGE' &&
+            !c.judgeId &&
+            (isAccuser || isRespondent) &&
+            (status === 'OPEN' || status === 'IN_PROGRESS');
+          const canIssueVerdict =
+            (isJudge || isDictator || isMediator) &&
+            status === 'OPEN';
+          const canProposeSettlement =
+            (isAccuser || isRespondent || isMediator) &&
+            method === 'MEDIATION' &&
+            (status === 'OPEN' || status === 'IN_PROGRESS');
+          const canAddEvidence =
+            (isAccuser ||
+              isRespondent ||
+              isMediator ||
+              isJudge ||
+              isDictator) &&
+            (status === 'OPEN' || status === 'IN_PROGRESS');
+          return (
+            canAnswer ||
+            canAssignJudge ||
+            canIssueVerdict ||
+            canProposeSettlement ||
+            canAddEvidence
+          );
+        });
+      }
+      state.myCases = myCases;
+    }
+    if (filter === 'judges') {
+      const nominations = await courtsModel.listNominations();
+      state.nominations = nominations || [];
+    }
+    if (filter === 'history') {
+      let history = await courtsModel.listCases('history');
+      history = history.map((c) => {
+        const id = String(currentUserId || '');
+        const isAccuser = String(c.accuser || '') === id;
+        const isRespondent = String(c.respondentId || '') === id;
+        const ma = Array.isArray(c.mediatorsAccuser)
+          ? c.mediatorsAccuser
+          : [];
+        const mr = Array.isArray(c.mediatorsRespondent)
+          ? c.mediatorsRespondent
+          : [];
+        const isMediator = ma.includes(id) || mr.includes(id);
+        const isJudge = String(c.judgeId || '') === id;
+        const mine = isAccuser || isRespondent || isMediator || isJudge;
+        const publicDetails =
+          c.publicPrefAccuser === true &&
+          c.publicPrefRespondent === true;
+        const decidedAt =
+          c.verdictAt ||
+          c.closedAt ||
+          c.decidedAt;
+        return {
+          ...c,
+          respondent: c.respondentId || c.respondent,
+          mine,
+          publicDetails,
+          decidedAt
+        };
+      });
+      if (search) {
+        const sLower = search.toLowerCase();
+        history = history.filter((c) => {
+          const t = String(c.title || '').toLowerCase();
+          const d = String(c.description || '').toLowerCase();
+          return t.includes(sLower) || d.includes(sLower);
+        });
+      }
+      state.history = history;
+    }
+    ctx.body = await courtsView(state);
+  })
+  .get('/courts/cases/:id', async (ctx) => {
+    const mod = ctx.cookies.get('courtsMod') || 'on';
+    if (mod !== 'on') {
+      ctx.redirect('/modules');
+      return;
+    }
+    const caseId = ctx.params.id;
+    let caseData = null;
+    try {
+      caseData = await courtsModel.getCaseDetails({ caseId });
+    } catch (e) {
+      caseData = null;
+    }
+    const state = { caseData };
+    ctx.body = await courtsCaseView(state);
+  })
   .get('/tribes', async ctx => {
   .get('/tribes', async ctx => {
     const filter = ctx.query.filter || 'all';
     const filter = ctx.query.filter || 'all';
     const search = ctx.query.search || ''; 
     const search = ctx.query.search || ''; 
@@ -2740,6 +2890,305 @@ router
     await parliamentModel.createRevocation({ lawId, title, reasons });
     await parliamentModel.createRevocation({ lawId, title, reasons });
     ctx.redirect('/parliament?filter=revocations');
     ctx.redirect('/parliament?filter=revocations');
   })
   })
+  .post('/courts/cases/create', koaBody(), async (ctx) => {
+    const body = ctx.request.body || {};
+    const titleSuffix = String(body.titleSuffix || '').trim();
+    const titlePreset = String(body.titlePreset || '').trim();
+    const respondentRaw = String(body.respondentId || '').trim();
+    const methodRaw = String(body.method || '').trim().toUpperCase();
+    const ALLOWED = new Set(['JUDGE', 'DICTATOR', 'POPULAR', 'MEDIATION', 'KARMATOCRACY']);
+    if (!titleSuffix && !titlePreset) {
+      ctx.flash = { message: 'Title is required.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    if (!respondentRaw) {
+      ctx.flash = { message: 'Accused / Respondent is required.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    if (!ALLOWED.has(methodRaw)) {
+      ctx.flash = { message: 'Invalid resolution method.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    const parts = [];
+    if (titlePreset) parts.push(titlePreset);
+    if (titleSuffix) parts.push(titleSuffix);
+    const titleBase = parts.join(' - ');
+    try {
+      await courtsModel.openCase({
+        titleBase,
+        respondentInput: respondentRaw,
+        method: methodRaw
+      });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect('/courts?filter=mycases');
+  })
+  .post('/courts/cases/:id/evidence/add', koaBody({ multipart: true }), async (ctx) => {
+    const caseId = ctx.params.id;
+    const body = ctx.request.body || {};
+    const text = String(body.text || '');
+    const link = String(body.link || '');
+    if (!caseId) {
+      ctx.flash = { message: 'Case not found.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    try {
+      const imageMarkdown = ctx.request.files?.image ? await handleBlobUpload(ctx, 'image') : null;
+      await courtsModel.addEvidence({
+        caseId,
+        text,
+        link,
+        imageMarkdown
+      });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+  })
+  .post('/courts/cases/:id/answer', koaBody(), async (ctx) => {
+    const caseId = ctx.params.id;
+    const body = ctx.request.body || {};
+    const answer = String(body.answer || '');
+    const stance = String(body.stance || '').toUpperCase();
+    const ALLOWED = new Set(['DENY', 'ADMIT', 'PARTIAL']);
+    if (!caseId) {
+      ctx.flash = { message: 'Case not found.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    if (!answer) {
+      ctx.flash = { message: 'Response brief is required.' };
+      ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+      return;
+    }
+    if (!ALLOWED.has(stance)) {
+      ctx.flash = { message: 'Invalid stance.' };
+      ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+      return;
+    }
+    try {
+      await courtsModel.answerCase({ caseId, stance, text: answer });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+  })
+  .post('/courts/cases/:id/decide', koaBody(), async (ctx) => {
+    const caseId = ctx.params.id;
+    const body = ctx.request.body || {};
+    const result = String(body.outcome || '').trim();
+    const orders = String(body.orders || '');
+    if (!caseId) {
+      ctx.flash = { message: 'Case not found.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    if (!result) {
+      ctx.flash = { message: 'Result is required.' };
+      ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+      return;
+    }
+    try {
+      await courtsModel.issueVerdict({ caseId, result, orders });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+  })
+  .post('/courts/cases/:id/settlements/propose', koaBody(), async (ctx) => {
+    const caseId = ctx.params.id;
+    const body = ctx.request.body || {};
+    const terms = String(body.terms || '');
+    if (!caseId) {
+      ctx.flash = { message: 'Case not found.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    if (!terms) {
+      ctx.flash = { message: 'Terms are required.' };
+      ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+      return;
+    }
+    try {
+      await courtsModel.proposeSettlement({ caseId, terms });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+  })
+  .post('/courts/cases/:id/settlements/accept', koaBody(), async (ctx) => {
+    const caseId = ctx.params.id;
+    if (!caseId) {
+      ctx.flash = { message: 'Case not found.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    try {
+      await courtsModel.acceptSettlement({ caseId });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+  })
+  .post('/courts/cases/:id/mediators/accuser', koaBody(), async (ctx) => {
+    const caseId = ctx.params.id;
+    const body = ctx.request.body || {};
+    const raw = String(body.mediators || '');
+    if (!caseId) {
+      ctx.flash = { message: 'Case not found.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    const mediators = raw
+      .split(',')
+      .map((s) => s.trim())
+      .filter((s) => s.length > 0);
+    if (!mediators.length) {
+      ctx.flash = { message: 'At least one mediator is required.' };
+      ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+      return;
+    }
+    const currentUserId = ctx.state && ctx.state.user && ctx.state.user.id;
+    if (currentUserId && mediators.includes(currentUserId)) {
+      ctx.flash = { message: 'You cannot appoint yourself as mediator.' };
+      ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+      return;
+    }
+    try {
+      await courtsModel.setMediators({ caseId, side: 'accuser', mediators });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+  })
+  .post('/courts/cases/:id/mediators/respondent', koaBody(), async (ctx) => {
+    const caseId = ctx.params.id;
+    const body = ctx.request.body || {};
+    const raw = String(body.mediators || '');
+    if (!caseId) {
+      ctx.flash = { message: 'Case not found.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    const mediators = raw
+      .split(',')
+      .map((s) => s.trim())
+      .filter((s) => s.length > 0);
+    if (!mediators.length) {
+      ctx.flash = { message: 'At least one mediator is required.' };
+      ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+      return;
+    }
+    const currentUserId = ctx.state && ctx.state.user && ctx.state.user.id;
+    if (currentUserId && mediators.includes(currentUserId)) {
+      ctx.flash = { message: 'You cannot appoint yourself as mediator.' };
+      ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+      return;
+    }
+    try {
+      await courtsModel.setMediators({ caseId, side: 'respondent', mediators });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+  })
+  .post('/courts/cases/:id/judge', koaBody(), async (ctx) => {
+    const caseId = ctx.params.id;
+    const body = ctx.request.body || {};
+    const judgeId = String(body.judgeId || '').trim();
+    if (!caseId) {
+      ctx.flash = { message: 'Case not found.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    if (!judgeId) {
+      ctx.flash = { message: 'Judge is required.' };
+      ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+      return;
+    }
+    const currentUserId = ctx.state && ctx.state.user && ctx.state.user.id;
+    if (currentUserId && judgeId === currentUserId) {
+      ctx.flash = { message: 'You cannot assign yourself as judge.' };
+      ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+      return;
+    }
+    try {
+      await courtsModel.assignJudge({ caseId, judgeId });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+  })
+  .post('/courts/cases/:id/public', koaBody(), async (ctx) => {
+    const caseId = ctx.params.id;
+    const body = ctx.request.body || {};
+    const pref = String(body.preference || '').toUpperCase();
+    if (!caseId) {
+      ctx.flash = { message: 'Case not found.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    if (pref !== 'YES' && pref !== 'NO') {
+      ctx.flash = { message: 'Invalid visibility preference.' };
+      ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+      return;
+    }
+    const preference = pref === 'YES';
+    try {
+      await courtsModel.setPublicPreference({ caseId, preference });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+  })
+  .post('/courts/cases/:id/openVote', koaBody(), async (ctx) => {
+    const caseId = ctx.params.id;
+    if (!caseId) {
+      ctx.flash = { message: 'Case not found.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    try {
+      await courtsModel.openPopularVote({ caseId });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+  })
+  .post('/courts/judges/nominate', koaBody(), async (ctx) => {
+    const body = ctx.request.body || {};
+    const judgeId = String(body.judgeId || '').trim();
+    if (!judgeId) {
+      ctx.flash = { message: 'Judge is required.' };
+      ctx.redirect('/courts?filter=judges');
+      return;
+    }
+    try {
+      await courtsModel.nominateJudge({ judgeId });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect('/courts?filter=judges');
+  })
+  .post('/courts/judges/:id/vote', koaBody(), async (ctx) => {
+    const nominationId = ctx.params.id;
+    if (!nominationId) {
+      ctx.flash = { message: 'Nomination not found.' };
+      ctx.redirect('/courts?filter=judges');
+      return;
+    }
+    try {
+      await courtsModel.voteNomination(nominationId);
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect('/courts?filter=judges');
+  })  
   .post('/market/create', koaBody({ multipart: true }), async ctx => {
   .post('/market/create', koaBody({ multipart: true }), async ctx => {
     const { item_type, title, description, price, tags, item_status, deadline, includesShipping, stock } = ctx.request.body;
     const { item_type, title, description, price, tags, item_status, deadline, includesShipping, stock } = ctx.request.body;
     const image = await handleBlobUpload(ctx, 'image');
     const image = await handleBlobUpload(ctx, 'image');
@@ -3324,7 +3773,7 @@ router
     'popular', 'topics', 'summaries', 'latest', 'threads', 'multiverse', 'invites', 'wallet',
     'popular', 'topics', 'summaries', 'latest', 'threads', 'multiverse', 'invites', 'wallet',
     'legacy', 'cipher', 'bookmarks', 'videos', 'docs', 'audios', 'tags', 'images', 'trending',
     'legacy', 'cipher', 'bookmarks', 'videos', 'docs', 'audios', 'tags', 'images', 'trending',
     'events', 'tasks', 'market', 'tribes', 'votes', 'reports', 'opinions', 'transfers',
     'events', 'tasks', 'market', 'tribes', 'votes', 'reports', 'opinions', 'transfers',
-    'feed', 'pixelia', 'agenda', 'ai', 'forum', 'jobs', 'projects', 'banking', 'parliament'
+    'feed', 'pixelia', 'agenda', 'ai', 'forum', 'jobs', 'projects', 'banking', 'parliament', 'courts'
     ];
     ];
     const currentConfig = getConfig();
     const currentConfig = getConfig();
     modules.forEach(mod => {
     modules.forEach(mod => {

+ 248 - 22
src/client/assets/translations/oasis_en.js

@@ -733,6 +733,7 @@ module.exports = {
     typeParliamentTerm: "Parliament · Term",
     typeParliamentTerm: "Parliament · Term",
     typeParliamentProposal: "Parliament · Proposal",
     typeParliamentProposal: "Parliament · Proposal",
     typeParliamentLaw: "Parliament · New Law",
     typeParliamentLaw: "Parliament · New Law",
+    typeCourts: "Courts",
     parliamentLawQuestion: "Question",
     parliamentLawQuestion: "Question",
     parliamentStatus: "Status",
     parliamentStatus: "Status",
     parliamentCandidaturesListTitle: "List of Candidatures",
     parliamentCandidaturesListTitle: "List of Candidatures",
@@ -760,6 +761,121 @@ module.exports = {
     parliamentRulesLaws: "When a proposal meets its threshold, it becomes a Law and appears in the Laws tab with its enactment date.",
     parliamentRulesLaws: "When a proposal meets its threshold, it becomes a Law and appears in the Laws tab with its enactment date.",
     parliamentRulesHistorical: "In the Historical tab you can see every government cycle that has occurred and data about its management.",
     parliamentRulesHistorical: "In the Historical tab you can see every government cycle that has occurred and data about its management.",
     parliamentRulesLeaders: "In the Leaders tab you can see a ranking of inhabitants/tribes that have governed (or stood as candidates), ordered by efficiency.",
     parliamentRulesLeaders: "In the Leaders tab you can see a ranking of inhabitants/tribes that have governed (or stood as candidates), ordered by efficiency.",
+    //courts
+    courtsTitle: "Courts",
+    courtsDescription: "Explore forms of conflict resolution and collective justice management.",
+    courtsFilterCases: "CASES",
+    courtsFilterMyCases: "MINE",
+    courtsFilterJudges: "JUDGES",
+    courtsFilterHistory: "HISTORY",
+    courtsFilterRules: "RULES",
+    courtsFilterOpenCase: "OPEN CASE",
+    courtsCaseFormTitle: "Open Case",
+    courtsCaseTitle: "Title",
+    courtsCaseRespondent: "Accused / Respondent",
+    courtsCaseRespondentPh: "Oasis ID (@...) or Tribe name",
+    courtsCaseMediatorsAccuser: "Mediators (accuser)",
+    courtsCaseMediatorsPh: "Oasis IDs, separated by comma",
+    courtsCaseMethod: "Resolution method",
+    courtsCaseDescription: "Description (max 1000 chars.)",
+    courtsCaseEvidenceTitle: "Case evidence",
+    courtsCaseEvidenceHelp: "Attach images, audios, documents (PDF) or videos that support your case.",
+    courtsCaseSubmit: "File Case",
+    courtsNominateJudge: "Nominate Judge",
+    courtsJudgeId: "Judge",
+    courtsJudgeIdPh: "Oasis ID (@...) or Inhabitant name",
+    courtsNominateBtn: "Nominate",
+    courtsAddEvidence: "Add evidence",
+    courtsEvidenceText: "Text",
+    courtsEvidenceLink: "Link",
+    courtsEvidenceLinkPh: "https://…",
+    courtsEvidenceSubmit: "Attach",
+    courtsAnswerTitle: "Answer the claim",
+    courtsAnswerText: "Response brief",
+    courtsAnswerSubmit: "Send response",
+    courtsStanceDENY: "Deny",
+    courtsStanceADMIT: "Admit",
+    courtsStancePARTIAL: "Partial",
+    courtsVerdictTitle: "Issue verdict",
+    courtsVerdictResult: "Result",
+    courtsVerdictOrders: "Orders",
+    courtsVerdictOrdersPh: "Actions, deadlines, restorative steps ...",
+    courtsIssueVerdict: "Issue verdict",
+    courtsMediationPropose: "Propose settlement",
+    courtsSettlementText: "Terms",
+    courtsSettlementProposeBtn: "Propose",
+    courtsNominationsTitle: "Judiciary nominations",
+    courtsThJudge: "Judge",
+    courtsThSupports: "Supports",
+    courtsThDate: "Date",
+    courtsThVote: "Vote",
+    courtsNoNominations: "No nominations yet.",
+    courtsAccuser: "Accuser",
+    courtsRespondent: "Respondent",
+    courtsThStatus: "Status",
+    courtsThAnswerBy: "Answer by",
+    courtsThEvidenceBy: "Evidence by",
+    courtsThDecisionBy: "Decision by",
+    courtsThCase: "Case",
+    courtsThCreatedAt: "Start date",
+    courtsThActions: "Actions",
+    courtsCaseMediatorsRespondentTitle: "Add defence mediators",
+    courtsCaseMediatorsRespondent: "Mediators (defence)",
+    courtsMediatorsAccuserLabel: "Mediators (accuser)",
+    courtsMediatorsRespondentLabel: "Mediators (defence)",
+    courtsMediatorsSubmit: "Save mediators",
+    courtsVotesNeeded: "Votes needed",
+    courtsVotesSlashTotal: "YES / TOTAL",
+    courtsOpenVote: "Open vote",
+    courtsPublicPrefLabel: "Visibility after resolution",
+    courtsPublicPrefYes: "I agree this case can be fully public",
+    courtsPublicPrefNo: "I prefer to keep the details private",
+    courtsPublicPrefSubmit: "Save visibility preference",
+    courtsNoCases: "No cases.",
+    courtsNoMyCases: "You have no conflicts yet.",
+    courtsNoHistory: "No recorded trials yet.",
+    courtsMethodJUDGE: "Judge",
+    courtsMethodDICTATOR: "Dictator",
+    courtsMethodPOPULAR: "Popular",
+    courtsMethodMEDIATION: "Mediation",
+    courtsMethodKARMATOCRACY: "Karmatocracy",
+    courtsMethod: "Method",
+    courtsRulesTitle: "How Courts work",
+    courtsRulesIntro: "Courts are a community-run process to resolve conflicts and promote restorative justice. Dialogue, clear evidence, and proportional remedies are prioritized.",
+    courtsRulesLifecycle: "Process: 1) Open case  2) Select method  3) Submit evidence  4) Hearing and deliberation  5) Verdict and remedy  6) Compliance and closure  7) Appeal (if applicable).",
+    courtsRulesRoles: "Accuser: opens the case. Defence: accused person or tribe. Method: mechanism chosen by the community to facilitate, assess evidence, and issue a verdict. Witness: provides testimony or evidence. Mediators: neutral people invited by the accuser and/or the defence, with access to all details, who help de-escalate the conflict and co-create agreements.",
+    courtsRulesEvidence: "Description up to 1000 characters. Attach relevant and lawful images, audio, video, and PDF documents. Do not share sensitive private data without consent.",
+    courtsRulesDeliberation: "Hearings may be public or private. Judges ensure respect, request clarifications, and may discard irrelevant or unlawful material.",
+    courtsRulesVerdict: "Restoration is prioritized: apologies, mediation agreements, content moderation, temporary restrictions, or other proportional measures. The reasoning must be recorded.",
+    courtsRulesAppeals: "Appeal: allowed when there is new evidence or a clear procedural error. Must be filed within 7 days unless otherwise stated.",
+    courtsRulesPrivacy: "Respect privacy and safety. Doxing, hate, or threats are removed. Judges may edit or seal parts of the record to protect people at risk.",
+    courtsRulesMisconduct: "Harassment, manipulation, or fabricated evidence may lead to immediate negative resolution.",
+    courtsRulesGlossary: "Case: record of a conflict. Evidence: materials that support claims. Verdict: decision with remedy. Appeal: request to review the verdict.",
+    courtsFilterActions: "ACTIONS",
+    courtsNoActions: "No pending actions for your role.",
+    courtsCaseTitlePlaceholder: "Short description of the conflict",
+    courtsCaseSeverity: "Severity",
+    courtsCaseSeverityNone: "No severity tag",
+    courtsCaseSeverityLOW: "Low",
+    courtsCaseSeverityMEDIUM: "Medium",
+    courtsCaseSeverityHIGH: "High",
+    courtsCaseSeverityCRITICAL: "Critical",
+    courtsCaseSubject: "Topic",
+    courtsCaseSubjectNone: "No topic tag",
+    courtsCaseSubjectBEHAVIOUR: "Behaviour",
+    courtsCaseSubjectCONTENT: "Content",
+    courtsCaseSubjectGOVERNANCE: "Governance / rules",
+    courtsCaseSubjectFINANCIAL: "Financial / resources",
+    courtsCaseSubjectOTHER: "Other",
+    courtsHiddenRespondent: "Hidden (only visible to involved roles).",
+    courtsThRole: "Role",
+    courtsRoleAccuser: "Accuser",
+    courtsRoleDefence: "Defence",
+    courtsRoleMediator: "Mediator",
+    courtsRoleJudge: "Judge",
+    courtsRoleDictator: "Dictator",
+    courtsAssignJudgeTitle: "Choose judge",
+    courtsAssignJudgeBtn: "Choose judge",
     //trending
     //trending
     trendingTitle: "Trending",
     trendingTitle: "Trending",
     exploreTrending: "Explore the most popular content in your network.",
     exploreTrending: "Explore the most popular content in your network.",
@@ -1185,16 +1301,16 @@ module.exports = {
     allButton:            "ALL",
     allButton:            "ALL",
     mineButton:           "MINE",
     mineButton:           "MINE",
     noActions:            "No activity available.",
     noActions:            "No activity available.",
-    performed:            "→",   
-    from: "From",
-    to: "To",
-    amount: "Amount",
-    concept: "Concept",
-    description: "Description",
-    meme: "Meme",
-    activityContact: "Contact",
-    activityBy: "Name",
-    activityPixelia: "New pixel added",
+    performed:            "→",
+    from:                 "From",
+    to:                   "To",
+    amount:               "Amount",
+    concept:              "Concept",
+    description:          "Description",
+    meme:                 "Meme",
+    activityContact:      "Contact",
+    activityBy:           "Name",
+    activityPixelia:      "New pixel added",
     viewImage:            "View image",
     viewImage:            "View image",
     playAudio:            "Play audio",
     playAudio:            "Play audio",
     playVideo:            "Play video",
     playVideo:            "Play video",
@@ -1217,14 +1333,33 @@ module.exports = {
     typeEvent:            "EVENTS",
     typeEvent:            "EVENTS",
     typeTransfer:         "TRANSFER",
     typeTransfer:         "TRANSFER",
     typeTask:             "TASKS",
     typeTask:             "TASKS",
-    typePixelia: 	  "PIXELIA",
-    typeForum: 	          "FORUM",
+    typePixelia:          "PIXELIA",
+    typeForum:            "FORUM",
     typeReport:           "REPORTS",
     typeReport:           "REPORTS",
     typeFeed:             "FEED",
     typeFeed:             "FEED",
     typeContact:          "CONTACT",
     typeContact:          "CONTACT",
     typePub:              "PUB",
     typePub:              "PUB",
-    typeTombstone:	  "TOMBSTONE",
+    typeTombstone:        "TOMBSTONE",
     typeBanking:          "BANKING",
     typeBanking:          "BANKING",
+    typeBankWallet:       "BANKING/WALLET",
+    typeBankClaim:        "BANKING/UBI",
+    typeKarmaScore:       "KARMA",
+    typeParliament:       "PARLIAMENT",
+    typeParliamentCandidature: "Parliament · Candidature",
+    typeParliamentTerm:   "Parliament · Term",
+    typeParliamentProposal:"Parliament · Proposal",
+    typeParliamentRevocation:"Parliament · Revocation",
+    typeParliamentLaw:    "Parliament · New Law",
+    typeCourts:           "COURTS",
+    typeCourtsCase:       "Courts · Case",
+    typeCourtsEvidence:   "Courts · Evidence",
+    typeCourtsAnswer:     "Courts · Answer",
+    typeCourtsVerdict:    "Courts · Verdict",
+    typeCourtsSettlement: "Courts · Settlement",
+    typeCourtsSettlementProposal: "Courts · Settlement Proposal",
+    typeCourtsSettlementAccepted: "Courts · Settlement Accepted",
+    typeCourtsNomination: "Courts · Nomination",
+    typeCourtsNominationVote: "Courts · Nomination Vote",
     activitySupport:      "New alliance forged",
     activitySupport:      "New alliance forged",
     activityJoin:         "New PUB joined",
     activityJoin:         "New PUB joined",
     question:             "Question",
     question:             "Question",
@@ -1232,6 +1367,7 @@ module.exports = {
     status:               "Status",
     status:               "Status",
     votes:                "Votes",
     votes:                "Votes",
     totalVotes:           "Total Votes",
     totalVotes:           "Total Votes",
+    voteTotalVotes:       "Total Votes",
     name:                 "Name",
     name:                 "Name",
     skills:               "Skills",
     skills:               "Skills",
     tags:                 "Tags",
     tags:                 "Tags",
@@ -1242,23 +1378,94 @@ module.exports = {
     activitySpread:       "->",
     activitySpread:       "->",
     visitLink:            "Visit Link",
     visitLink:            "Visit Link",
     viewDocument:         "View Document",
     viewDocument:         "View Document",
-    description:          "Description",
     location:             "Location",
     location:             "Location",
     contentWarning:       "Subject",
     contentWarning:       "Subject",
     personName:           "Inhabitant Name",
     personName:           "Inhabitant Name",
-    typeBankWallet:       "BANKING/WALLET",
-    typeBankClaim:        "BANKING/UBI",
-    typeKarmaScore:	  "KARMA",
     bankWalletConnected:  "ECOin Wallet",
     bankWalletConnected:  "ECOin Wallet",
     bankUbiReceived:      "UBI Received",
     bankUbiReceived:      "UBI Received",
     bankTx:               "Tx",
     bankTx:               "Tx",
     bankEpochShort:       "Epoch",
     bankEpochShort:       "Epoch",
-    activityProjectFollow: "%OASIS% is now %ACTION% this project %PROJECT%",
+    bankAllocId:          "Allocation ID",
+    bankingUserEngagementScore: "User Engagement Score",
+    viewDetails:          "View details",
+    link:                 "Link",
+    aiSnippetsLearned:    "Snippets learned",
+    tribeFeedRefeeds:     "Refeeds",
+    activityProjectFollow:   "%OASIS% is now %ACTION% this project %PROJECT%",
     activityProjectUnfollow: "%OASIS% is now %ACTION% this project %PROJECT%",
     activityProjectUnfollow: "%OASIS% is now %ACTION% this project %PROJECT%",
-    activityProjectPledged: "%OASIS% has %ACTION% %AMOUNT% to project %PROJECT%",
-    following: "FOLLOWING",
-    unfollowing: "UNFOLLOWING",
-    pledged: "PLEDGED",
+    activityProjectPledged:  "%OASIS% has %ACTION% %AMOUNT% to project %PROJECT%",
+    following:            "FOLLOWING",
+    unfollowing:          "UNFOLLOWING",
+    pledged:              "PLEDGED",
+    parliamentCandidatureId: "Candidature",
+    parliamentGovMethod:     "Method",
+    parliamentVotesReceived: "Votes received",
+    parliamentMethodANARCHY:   "Anarchy",
+    parliamentMethodVOTE:      "Community Vote",
+    parliamentMethodRANKED:    "Ranked Choice",
+    parliamentMethodPLURALITY: "Plurality",
+    parliamentMethodCOUNCIL:   "Council",
+    parliamentMethodJURY:      "Jury",
+    parliamentAnarchy:         "ANARCHY",
+    parliamentElectionsStart:  "Elections start",
+    parliamentElectionsEnd:    "Elections end",
+    parliamentCurrentLeader:   "Winning candidature",
+    parliamentProposalTitle:   "Title",
+    parliamentOpenVote:        "Open vote",
+    parliamentStatus:          "Status",
+    parliamentLawQuestion:     "Question",
+    parliamentLawMethod:       "Method",
+    parliamentLawProposer:     "Proposer",
+    parliamentLawEnacted:      "Enacted at",
+    parliamentLawVotes:        "Votes",
+    createdAt:                 "Created at",
+    courtsCaseTitle:           "Case",
+    courtsMethod:              "Method",
+    courtsMethodJUDGE:         "Judge",
+    courtsMethodJUDGES:        "Judges Panel",
+    courtsMethodSINGLE_JUDGE:  "Single Judge",
+    courtsMethodJURY:          "Jury",
+    courtsMethodCOUNCIL:       "Council",
+    courtsMethodCOMMUNITY:     "Community",
+    courtsMethodMEDIATION:     "Mediation",
+    courtsMethodARBITRATION:   "Arbitration",
+    courtsMethodVOTE:          "Community Vote",
+    courtsAccuser:             "Accuser",
+    courtsRespondent:          "Respondent",
+    courtsThStatus:            "Status",
+    courtsThAnswerBy:          "Answer by",
+    courtsThEvidenceBy:        "Evidence by",
+    courtsThDecisionBy:        "Decision by",
+    courtsVotesNeeded:         "Votes needed",
+    courtsVotesSlashTotal:     "YES/TOTAL",
+    courtsOpenVote:            "Open vote",
+    courtsAnswerTitle:         "Answer",
+    courtsStanceADMIT:         "Admit",
+    courtsStanceDENY:          "Deny",
+    courtsStancePARTIAL:       "Partial",
+    courtsStanceCOUNTERCLAIM:  "Counterclaim",
+    courtsStanceNEUTRAL:       "Neutral",
+    courtsVerdictResult:       "Result",
+    courtsVerdictOrders:       "Orders",
+    courtsSettlementText:      "Settlement",
+    courtsSettlementAccepted:  "Accepted",
+    courtsSettlementPending:   "Pending",
+    courtsJudge:               "Judge",
+    courtsThSupports:          "Supports",
+    courtsFilterOpenCase: 'Open Case',
+    courtsEvidenceFileLabel: 'Evidence file (image, audio, video or PDF)',
+    courtsCaseMediators: 'Mediators',
+    courtsCaseMediatorsPh: 'Mediator Oasis IDs, separated by comma',
+    courtsMediatorsLabel: 'Mediators',
+    courtsThCase: 'Case',
+    courtsThCreatedAt: 'Start date',
+    courtsThActions: 'Actions',
+    courtsPublicPrefLabel: 'Visibility after resolution',
+    courtsPublicPrefYes: 'I agree this case can be fully public',
+    courtsPublicPrefNo: 'I prefer to keep the details private',
+    courtsPublicPrefSubmit: 'Save visibility preference',
+    courtsMethodMEDIATION: 'Mediation',
+    courtsNoCases: 'No cases.',
     //reports
     //reports
     reportsTitle: "Reports",
     reportsTitle: "Reports",
     reportsDescription: "Manage and track reports related to issues, bugs, abuses and content warnings in your network.",
     reportsDescription: "Manage and track reports related to issues, bugs, abuses and content warnings in your network.",
@@ -1700,6 +1907,23 @@ module.exports = {
     statsTombstoneTitle: "Tombstones",
     statsTombstoneTitle: "Tombstones",
     statsNetworkTombstones: "Network tombstones",
     statsNetworkTombstones: "Network tombstones",
     statsTombstoneRatio: "Tombstone ratio (%)",
     statsTombstoneRatio: "Tombstone ratio (%)",
+    statsAITraining: "AI Training",
+    statsAIExchanges: "Exchanges",
+    bankingUserEngagementScore: "KARMA Score",
+    statsParliamentCandidature: "Parliament candidatures",
+    statsParliamentTerm: "Parliament terms",
+    statsParliamentProposal: "Parliament proposals",
+    statsParliamentRevocation: "Parliament revocations",
+    statsParliamentLaw: "Parliament laws",
+    statsCourtsCase: "Court cases",
+    statsCourtsEvidence: "Court evidence",
+    statsCourtsAnswer: "Court answers",
+    statsCourtsVerdict: "Court verdicts",
+    statsCourtsSettlement: "Court settlements",
+    statsCourtsSettlementProposal: "Settlement proposals",
+    statsCourtsSettlementAccepted: "Settlements accepted",
+    statsCourtsNomination: "Judge nominations",
+    statsCourtsNominationVote: "Nomination votes",
     //AI
     //AI
     ai: "AI",
     ai: "AI",
     aiTitle: "AI",
     aiTitle: "AI",
@@ -2010,6 +2234,8 @@ module.exports = {
     modulesFeedDescription: "Module to discover and share short-texts (feeds).",
     modulesFeedDescription: "Module to discover and share short-texts (feeds).",
     modulesParliamentLabel: "Parliament",
     modulesParliamentLabel: "Parliament",
     modulesParliamentDescription: "Module to elect governments and vote on laws.",
     modulesParliamentDescription: "Module to elect governments and vote on laws.",
+    modulesCourtsLabel: "Courts",
+    modulesCourtsDescription: "Module to resolve conflicts and emit veredicts.",
     modulesPixeliaLabel: "Pixelia",
     modulesPixeliaLabel: "Pixelia",
     modulesPixeliaDescription: "Module to draw on a collaborative grid.",
     modulesPixeliaDescription: "Module to draw on a collaborative grid.",
     modulesAgendaLabel: "Agenda",
     modulesAgendaLabel: "Agenda",

+ 285 - 72
src/client/assets/translations/oasis_es.js

@@ -756,6 +756,121 @@ module.exports = {
     parliamentRulesLaws: "Cuando una propuesta alcanza su umbral, se convierte en Ley y aparece en la pestaña Leyes con su fecha de entrada en vigor.",
     parliamentRulesLaws: "Cuando una propuesta alcanza su umbral, se convierte en Ley y aparece en la pestaña Leyes con su fecha de entrada en vigor.",
     parliamentRulesHistorical: "En Histórico se puede ver cada ciclo de gobierno que ha habido y datos sobre su gestión.",
     parliamentRulesHistorical: "En Histórico se puede ver cada ciclo de gobierno que ha habido y datos sobre su gestión.",
     parliamentRulesLeaders: "En Líderes se puede ver un ranking de habitantes/tribus que han gobernado (o se han presentado), ordenados por eficacia.",
     parliamentRulesLeaders: "En Líderes se puede ver un ranking de habitantes/tribus que han gobernado (o se han presentado), ordenados por eficacia.",
+     //courts
+    courtsTitle: "Tribunales",
+    courtsDescription: "Explora formas de resolución de conflictos y de gestión colectiva de la justicia.",
+    courtsFilterCases: "CASOS",
+    courtsFilterMyCases: "MÍOS",
+    courtsFilterJudges: "JUECES",
+    courtsFilterHistory: "HISTORIAL",
+    courtsFilterRules: "REGLAS",
+    courtsFilterOpenCase: "NUEVO CASO",
+    courtsCaseFormTitle: "Abrir caso",
+    courtsCaseTitle: "Título",
+    courtsCaseRespondent: "Acusado / Parte demandada",
+    courtsCaseRespondentPh: "ID de Oasis (@...) o nombre de Tribu",
+    courtsCaseMediatorsAccuser: "Mediadores (acusación)",
+    courtsCaseMediatorsPh: "ID de Oasis, separados por comas",
+    courtsCaseMethod: "Método de resolución",
+    courtsCaseDescription: "Descripción (máx. 1000 caracteres)",
+    courtsCaseEvidenceTitle: "Pruebas del caso",
+    courtsCaseEvidenceHelp: "Adjunta imágenes, audios, documentos (PDF) o vídeos que apoyen tu caso.",
+    courtsCaseSubmit: "Presentar caso",
+    courtsNominateJudge: "Nominar juez",
+    courtsJudgeId: "Juez",
+    courtsJudgeIdPh: "ID de Oasis (@...) o nombre de Habitante",
+    courtsNominateBtn: "Nominar",
+    courtsAddEvidence: "Añadir pruebas",
+    courtsEvidenceText: "Texto",
+    courtsEvidenceLink: "Enlace",
+    courtsEvidenceLinkPh: "https://…",
+    courtsEvidenceSubmit: "Adjuntar",
+    courtsAnswerTitle: "Responder a la reclamación",
+    courtsAnswerText: "Resumen de la respuesta",
+    courtsAnswerSubmit: "Enviar respuesta",
+    courtsStanceDENY: "Negar",
+    courtsStanceADMIT: "Admitir",
+    courtsStancePARTIAL: "Parcial",
+    courtsVerdictTitle: "Emitir veredicto",
+    courtsVerdictResult: "Resultado",
+    courtsVerdictOrders: "Órdenes",
+    courtsVerdictOrdersPh: "Acciones, plazos, pasos restaurativos...",
+    courtsIssueVerdict: "Emitir veredicto",
+    courtsMediationPropose: "Proponer acuerdo",
+    courtsSettlementText: "Términos",
+    courtsSettlementProposeBtn: "Proponer",
+    courtsNominationsTitle: "Nominaciones a la judicatura",
+    courtsThJudge: "Juez",
+    courtsThSupports: "Apoyos",
+    courtsThDate: "Fecha",
+    courtsThVote: "Votar",
+    courtsNoNominations: "Todavía no hay nominaciones.",
+    courtsAccuser: "Acusación",
+    courtsRespondent: "Defensa",
+    courtsThStatus: "Estado",
+    courtsThAnswerBy: "Responder antes de",
+    courtsThEvidenceBy: "Aportar pruebas antes de",
+    courtsThDecisionBy: "Decisión antes de",
+    courtsThCase: "Caso",
+    courtsThCreatedAt: "Fecha de inicio",
+    courtsThActions: "Acciones",
+    courtsCaseMediatorsRespondentTitle: "Añadir mediadores de la defensa",
+    courtsCaseMediatorsRespondent: "Mediadores (defensa)",
+    courtsMediatorsAccuserLabel: "Mediadores (acusación)",
+    courtsMediatorsRespondentLabel: "Mediadores (defensa)",
+    courtsMediatorsSubmit: "Guardar mediadores",
+    courtsVotesNeeded: "Votos necesarios",
+    courtsVotesSlashTotal: "SÍ / TOTAL",
+    courtsOpenVote: "Abrir votación",
+    courtsPublicPrefLabel: "Visibilidad tras la resolución",
+    courtsPublicPrefYes: "Acepto que este caso sea totalmente público",
+    courtsPublicPrefNo: "Prefiero mantener los detalles en privado",
+    courtsPublicPrefSubmit: "Guardar preferencia de visibilidad",
+    courtsNoCases: "Sin casos.",
+    courtsNoMyCases: "Aún no tienes conflictos.",
+    courtsNoHistory: "Todavía no hay juicios registrados.",
+    courtsMethodJUDGE: "Juez",
+    courtsMethodDICTATOR: "Dictador",
+    courtsMethodPOPULAR: "Popular",
+    courtsMethodMEDIATION: "Mediación",
+    courtsMethodKARMATOCRACY: "Karmatocracia",
+    courtsMethod: "Método",
+    courtsRulesTitle: "Cómo funcionan los Tribunales",
+    courtsRulesIntro: "Los Tribunales son un proceso gestionado por la comunidad para resolver conflictos y promover la justicia restaurativa. Se priorizan el diálogo, las pruebas claras y los remedios proporcionales.",
+    courtsRulesLifecycle: "Proceso: 1) Abrir caso  2) Seleccionar método  3) Presentar pruebas  4) Audiencia y deliberación  5) Veredicto y remedio  6) Cumplimiento y cierre  7) Apelación (si procede).",
+    courtsRulesRoles: "Acusación: abre el caso. Defensa: persona o tribu acusada. Método: mecanismo elegido por la comunidad para facilitar, evaluar las pruebas y emitir veredicto. Testigo: aporta testimonio o pruebas. Mediadores: personas neutrales invitadas por la acusación y/o la defensa, con acceso a todos los detalles, que ayudan a desescalar el conflicto y co-crear acuerdos.",
+    courtsRulesEvidence: "Descripción de hasta 1000 caracteres. Adjunta imágenes, audio, vídeo y documentos PDF relevantes y legales. No compartas datos privados sensibles sin consentimiento.",
+    courtsRulesDeliberation: "Las audiencias pueden ser públicas o privadas. Los jueces garantizan el respeto, piden aclaraciones y pueden descartar material irrelevante o ilegal.",
+    courtsRulesVerdict: "Se prioriza la restauración: disculpas, acuerdos de mediación, moderación de contenido, restricciones temporales u otras medidas proporcionales. El razonamiento debe quedar registrado.",
+    courtsRulesAppeals: "Apelación: permitida cuando hay nuevas pruebas o un error procesal claro. Debe presentarse en un plazo de 7 días salvo que se indique lo contrario.",
+    courtsRulesPrivacy: "Respeta la privacidad y la seguridad. El doxing, el odio o las amenazas se eliminan. Los jueces pueden editar o sellar partes del expediente para proteger a personas en riesgo.",
+    courtsRulesMisconduct: "El acoso, la manipulación o las pruebas fabricadas pueden llevar a una resolución negativa inmediata.",
+    courtsRulesGlossary: "Caso: registro de un conflicto. Pruebas: materiales que respaldan las afirmaciones. Veredicto: decisión con remedio. Apelación: solicitud para revisar el veredicto.",
+    courtsFilterActions: "ACCIONES",
+    courtsNoActions: "No hay acciones pendientes para tu rol.",
+    courtsCaseTitlePlaceholder: "Breve descripción del conflicto",
+    courtsCaseSeverity: "Severidad",
+    courtsCaseSeverityNone: "Sin etiqueta de severidad",
+    courtsCaseSeverityLOW: "Baja",
+    courtsCaseSeverityMEDIUM: "Media",
+    courtsCaseSeverityHIGH: "Alta",
+    courtsCaseSeverityCRITICAL: "Crítica",
+    courtsCaseSubject: "Tema",
+    courtsCaseSubjectNone: "Sin etiqueta de tema",
+    courtsCaseSubjectBEHAVIOUR: "Comportamiento",
+    courtsCaseSubjectCONTENT: "Contenido",
+    courtsCaseSubjectGOVERNANCE: "Gobernanza / normas",
+    courtsCaseSubjectFINANCIAL: "Finanzas / recursos",
+    courtsCaseSubjectOTHER: "Otro",
+    courtsHiddenRespondent: "Oculto (solo visible para los roles implicados).",
+    courtsThRole: "Rol",
+    courtsRoleAccuser: "Acusación",
+    courtsRoleDefence: "Defensa",
+    courtsRoleMediator: "Mediador",
+    courtsRoleJudge: "Juez",
+    courtsRoleDictator: "Dictador",
+    courtsAssignJudgeTitle: "Elegir juez",
+    courtsAssignJudgeBtn: "Elegir juez",
     //trending
     //trending
     trendingTitle: "Tendencias",
     trendingTitle: "Tendencias",
     exploreTrending: "Explora el contenido más popular en tu red.",
     exploreTrending: "Explora el contenido más popular en tu red.",
@@ -1177,13 +1292,13 @@ module.exports = {
     yourActivity:         "Tu actividad",
     yourActivity:         "Tu actividad",
     globalActivity:       "Actividad global",
     globalActivity:       "Actividad global",
     activityList:         "Actividad",
     activityList:         "Actividad",
-    activityDesc:         "Consulta la actividad reciente de tu red.",
-    allButton:            "TODOS",
+    activityDesc:         "Consulta la actividad más reciente de tu red.",
+    allButton:            "TODO",
     mineButton:           "MÍAS",
     mineButton:           "MÍAS",
     noActions:            "No hay actividad disponible.",
     noActions:            "No hay actividad disponible.",
     performed:            "→",
     performed:            "→",
     from:                 "De",
     from:                 "De",
-    to:                   "Para",
+    to:                   "A",
     amount:               "Cantidad",
     amount:               "Cantidad",
     concept:              "Concepto",
     concept:              "Concepto",
     description:          "Descripción",
     description:          "Descripción",
@@ -1196,11 +1311,11 @@ module.exports = {
     playVideo:            "Reproducir vídeo",
     playVideo:            "Reproducir vídeo",
     typeRecent:           "RECIENTE",
     typeRecent:           "RECIENTE",
     errorActivity:        "Error al recuperar la actividad",
     errorActivity:        "Error al recuperar la actividad",
-    typePost:             "PUBLICACIONES",
+    typePost:             "PUBLICACIÓN",
     typeTribe:            "TRIBUS",
     typeTribe:            "TRIBUS",
     typeAbout:            "HABITANTES",
     typeAbout:            "HABITANTES",
-    typeCurriculum:       "CVs",
-    typeImage:            "IMAGENES",
+    typeCurriculum:       "CV",
+    typeImage:            "IMÁGENES",
     typeBookmark:         "MARCADORES",
     typeBookmark:         "MARCADORES",
     typeDocument:         "DOCUMENTOS",
     typeDocument:         "DOCUMENTOS",
     typeVotes:            "VOTACIONES",
     typeVotes:            "VOTACIONES",
@@ -1211,16 +1326,35 @@ module.exports = {
     typeVideo:            "VÍDEOS",
     typeVideo:            "VÍDEOS",
     typeVote:             "DIFUSIÓN",
     typeVote:             "DIFUSIÓN",
     typeEvent:            "EVENTOS",
     typeEvent:            "EVENTOS",
-    typeTransfer:         "TRANSFERENCIAS",
+    typeTransfer:         "TRANSFERENCIA",
     typeTask:             "TAREAS",
     typeTask:             "TAREAS",
     typePixelia:          "PIXELIA",
     typePixelia:          "PIXELIA",
-    typeForum:            "FOROS",
+    typeForum:            "FORO",
     typeReport:           "REPORTES",
     typeReport:           "REPORTES",
     typeFeed:             "FEED",
     typeFeed:             "FEED",
-    typeContact:          "CONTACTOS",
-    typePub:              "PUBs",
-    typeTombstone:        "TOMBSTONES",
+    typeContact:          "CONTACTO",
+    typePub:              "PUB",
+    typeTombstone:        "TOMBSTONE",
     typeBanking:          "BANCA",
     typeBanking:          "BANCA",
+    typeBankWallet:       "BANCA/MONEDERO",
+    typeBankClaim:        "BANCA/UBI",
+    typeKarmaScore:       "KARMA",
+    typeParliament:       "PARLAMENTO",
+    typeParliamentCandidature: "Parlamento · Candidatura",
+    typeParliamentTerm:   "Parlamento · Mandato",
+    typeParliamentProposal:"Parlamento · Propuesta",
+    typeParliamentRevocation:"Parlamento · Revocación",
+    typeParliamentLaw:    "Parlamento · Nueva ley",
+    typeCourts:           "TRIBUNALES",
+    typeCourtsCase:       "Tribunales · Caso",
+    typeCourtsEvidence:   "Tribunales · Prueba",
+    typeCourtsAnswer:     "Tribunales · Respuesta",
+    typeCourtsVerdict:    "Tribunales · Veredicto",
+    typeCourtsSettlement: "Tribunales · Acuerdo",
+    typeCourtsSettlementProposal: "Tribunales · Propuesta de acuerdo",
+    typeCourtsSettlementAccepted: "Tribunales · Acuerdo aceptado",
+    typeCourtsNomination: "Tribunales · Nominación",
+    typeCourtsNominationVote: "Tribunales · Votación de nominación",
     activitySupport:      "Nueva alianza forjada",
     activitySupport:      "Nueva alianza forjada",
     activityJoin:         "Nuevo PUB unido",
     activityJoin:         "Nuevo PUB unido",
     question:             "Pregunta",
     question:             "Pregunta",
@@ -1228,6 +1362,7 @@ module.exports = {
     status:               "Estado",
     status:               "Estado",
     votes:                "Votos",
     votes:                "Votos",
     totalVotes:           "Votos totales",
     totalVotes:           "Votos totales",
+    voteTotalVotes:       "Votos totales",
     name:                 "Nombre",
     name:                 "Nombre",
     skills:               "Habilidades",
     skills:               "Habilidades",
     tags:                 "Etiquetas",
     tags:                 "Etiquetas",
@@ -1241,19 +1376,91 @@ module.exports = {
     location:             "Ubicación",
     location:             "Ubicación",
     contentWarning:       "Asunto",
     contentWarning:       "Asunto",
     personName:           "Nombre del habitante",
     personName:           "Nombre del habitante",
-    typeBankWallet:       "BANCA/CARTERA",
-    typeBankClaim:        "BANCA/UBI",
-    typeKarmaScore:	  "KARMA",
-    bankWalletConnected:  "Cartera ECOin",
-    bankUbiReceived:      "UBI recibida",
+    bankWalletConnected:  "Monedero ECOin",
+    bankUbiReceived:      "UBI recibido",
     bankTx:               "Tx",
     bankTx:               "Tx",
     bankEpochShort:       "Época",
     bankEpochShort:       "Época",
-    activityProjectFollow: "%OASIS% ahora está %ACTION% este proyecto %PROJECT%",
+    bankAllocId:          "ID de asignación",
+    bankingUserEngagementScore: "Puntuación de compromiso",
+    viewDetails:          "Ver detalles",
+    link:                 "Enlace",
+    aiSnippetsLearned:    "Fragmentos aprendidos",
+    tribeFeedRefeeds:     "Reenvíos",
+    activityProjectFollow:   "%OASIS% ahora está %ACTION% este proyecto %PROJECT%",
     activityProjectUnfollow: "%OASIS% ahora está %ACTION% este proyecto %PROJECT%",
     activityProjectUnfollow: "%OASIS% ahora está %ACTION% este proyecto %PROJECT%",
-    activityProjectPledged: "%OASIS% ha %ACTION% %AMOUNT% al proyecto %PROJECT%",
-    following: "SIGUIENDO",
-    unfollowing: "DEJANDO DE SEGUIR",
-    pledged: "APORTADO",
+    activityProjectPledged:  "%OASIS% ha %ACTION% %AMOUNT% al proyecto %PROJECT%",
+    following:            "SIGUIENDO",
+    unfollowing:          "DEJANDO DE SEGUIR",
+    pledged:              "APORTADO",
+    parliamentCandidatureId: "Candidatura",
+    parliamentGovMethod:     "Método",
+    parliamentVotesReceived: "Votos recibidos",
+    parliamentMethodANARCHY:   "Anarquía",
+    parliamentMethodVOTE:      "Votación comunitaria",
+    parliamentMethodRANKED:    "Voto preferencial",
+    parliamentMethodPLURALITY: "Pluralidad",
+    parliamentMethodCOUNCIL:   "Consejo",
+    parliamentMethodJURY:      "Jurado",
+    parliamentAnarchy:         "ANARQUÍA",
+    parliamentElectionsStart:  "Inicio de elecciones",
+    parliamentElectionsEnd:    "Fin de elecciones",
+    parliamentCurrentLeader:   "Candidatura ganadora",
+    parliamentProposalTitle:   "Título",
+    parliamentOpenVote:        "Votación abierta",
+    parliamentStatus:          "Estado",
+    parliamentLawQuestion:     "Pregunta",
+    parliamentLawMethod:       "Método",
+    parliamentLawProposer:     "Proponente",
+    parliamentLawEnacted:      "Promulgada",
+    parliamentLawVotes:        "Votos",
+    createdAt:                 "Creado el",
+    courtsCaseTitle:           "Caso",
+    courtsMethod:              "Método",
+    courtsMethodJUDGE:         "Juez",
+    courtsMethodJUDGES:        "Panel de jueces",
+    courtsMethodSINGLE_JUDGE:  "Juez único",
+    courtsMethodJURY:          "Jurado",
+    courtsMethodCOUNCIL:       "Consejo",
+    courtsMethodCOMMUNITY:     "Comunidad",
+    courtsMethodMEDIATION:     "Mediación",
+    courtsMethodARBITRATION:   "Arbitraje",
+    courtsMethodVOTE:          "Votación comunitaria",
+    courtsAccuser:             "Acusación",
+    courtsRespondent:          "Defensa",
+    courtsThStatus:            "Estado",
+    courtsThAnswerBy:          "Responder antes de",
+    courtsThEvidenceBy:        "Pruebas hasta",
+    courtsThDecisionBy:        "Decisión antes de",
+    courtsVotesNeeded:         "Votos necesarios",
+    courtsVotesSlashTotal:     "SÍ/TOTAL",
+    courtsOpenVote:            "Votación abierta",
+    courtsAnswerTitle:         "Respuesta",
+    courtsStanceADMIT:         "Admite",
+    courtsStanceDENY:          "Niega",
+    courtsStancePARTIAL:       "Parcial",
+    courtsStanceCOUNTERCLAIM:  "Reconvención",
+    courtsStanceNEUTRAL:       "Neutral",
+    courtsVerdictResult:       "Resultado",
+    courtsVerdictOrders:       "Órdenes",
+    courtsSettlementText:      "Acuerdo",
+    courtsSettlementAccepted:  "Aceptado",
+    courtsSettlementPending:   "Pendiente",
+    courtsJudge:               "Juez",
+    courtsThSupports:          "Apoyos",
+    courtsFilterOpenCase: 'Abrir caso',
+    courtsEvidenceFileLabel: 'Archivo de prueba (imagen, audio, vídeo o PDF)',
+    courtsCaseMediators: 'Mediadores',
+    courtsCaseMediatorsPh: 'IDs de Oasis de mediadores, separados por comas',
+    courtsMediatorsLabel: 'Mediadores',
+    courtsThCase: 'Caso',
+    courtsThCreatedAt: 'Fecha de inicio',
+    courtsThActions: 'Acciones',
+    courtsPublicPrefLabel: 'Visibilidad tras la resolución',
+    courtsPublicPrefYes: 'Acepto que este caso sea totalmente público',
+    courtsPublicPrefNo: 'Prefiero mantener privados los detalles',
+    courtsPublicPrefSubmit: 'Guardar preferencia de visibilidad',
+    courtsMethodMEDIATION: 'Mediación',
+    courtsNoCases: 'No hay casos.',
     //reports
     //reports
     reportsTitle: "Informes",
     reportsTitle: "Informes",
     reportsDescription: "Gestiona y realiza un seguimiento de los informes relacionados con problemas, errores, abusos y advertencias de contenido en tu red.",
     reportsDescription: "Gestiona y realiza un seguimiento de los informes relacionados con problemas, errores, abusos y advertencias de contenido en tu red.",
@@ -1602,65 +1809,52 @@ module.exports = {
     //stats
     //stats
     statsTitle: 'Estadísticas',
     statsTitle: 'Estadísticas',
     statistics: "Estadísticas",
     statistics: "Estadísticas",
-    statsInhabitant: "Estadísticas de Habitantes",
-    statsDescription: "Descubre las estadísticas de tu red.",
-    ALLButton: "TODOS",
+    statsInhabitant: "Estadísticas del Habitante",
+    statsDescription: "Descubre estadísticas sobre tu red.",
+    ALLButton: "TODO",
     MINEButton: "MÍAS",
     MINEButton: "MÍAS",
-    TOMBSTONEButton: "TUMBAS",
+    TOMBSTONEButton: "ELIMINADOS",
     statsYou: "Tú",
     statsYou: "Tú",
     statsUserId: "ID de Oasis",
     statsUserId: "ID de Oasis",
     statsCreatedAt: "Creado el",
     statsCreatedAt: "Creado el",
     statsYourContent: "Contenido",
     statsYourContent: "Contenido",
     statsYourOpinions: "Opiniones",
     statsYourOpinions: "Opiniones",
-    statsYourTombstone: "Lápidas",
-    statsYourProject: "Proyectos",
-    statsDiscoveredProject: "Proyectos",
-    statsBankingTitle: "Banca",
-    statsEcoWalletLabel: "Cartera de ECOIN",
-    statsEcoWalletNotConfigured: "Sin configurar!",
-    statsTotalEcoAddresses: "Direcciones totales",
-    statsProject: "Proyectos",
+    statsYourTombstone: "Eliminados",
     statsNetwork: "Red",
     statsNetwork: "Red",
     statsTotalInhabitants: "Habitantes",
     statsTotalInhabitants: "Habitantes",
     statsDiscoveredTribes: "Tribus (Públicas)",
     statsDiscoveredTribes: "Tribus (Públicas)",
     statsPrivateDiscoveredTribes: "Tribus (Privadas)",
     statsPrivateDiscoveredTribes: "Tribus (Privadas)",
     statsNetworkContent: "Contenido",
     statsNetworkContent: "Contenido",
     statsYourMarket: "Mercado",
     statsYourMarket: "Mercado",
-    statsYourJob: "Trabajos",
+    statsYourJob: "Empleos",
+    statsYourProject: "Proyectos",
     statsYourTransfer: "Transferencias",
     statsYourTransfer: "Transferencias",
     statsYourForum: "Foros",   
     statsYourForum: "Foros",   
-    statsProject: "Proyectos",
-    statsProjectsTitle: "Proyectos",
-    statsProjectsTotal: "Total de proyectos",
-    statsProjectsActive: "Activos",
-    statsProjectsCompleted: "Completados",
-    statsProjectsPaused: "Pausados",
-    statsProjectsCancelled: "Cancelados",
-    statsProjectsGoalTotal: "Meta total",
-    statsProjectsPledgedTotal: "Total comprometido",
-    statsProjectsSuccessRate: "Tasa de éxito",
-    statsProjectsAvgProgress: "Progreso promedio",
-    statsProjectsMedianProgress: "Progreso medio",
-    statsProjectsActiveFundingAvg: "Promedio de financiación activa",
     statsNetworkOpinions: "Opiniones",
     statsNetworkOpinions: "Opiniones",
     statsDiscoveredMarket: "Mercado",
     statsDiscoveredMarket: "Mercado",
-    statsDiscoveredJob: "Trabajos",
+    statsDiscoveredJob: "Empleos",
+    statsDiscoveredProject: "Proyectos",
+    statsBankingTitle: "Banca",
+    statsEcoWalletLabel: "Billetera ECOIN",
+    statsEcoWalletNotConfigured:  "¡No configurada!",
+    statsTotalEcoAddresses: "Direcciones totales",
     statsDiscoveredTransfer: "Transferencias",
     statsDiscoveredTransfer: "Transferencias",
     statsDiscoveredForum: "Foros",
     statsDiscoveredForum: "Foros",
-    statsNetworkTombstone: "Lápidas",
+    statsNetworkTombstone: "Eliminados",
     statsBookmark: "Marcadores",
     statsBookmark: "Marcadores",
     statsEvent: "Eventos",
     statsEvent: "Eventos",
     statsTask: "Tareas",
     statsTask: "Tareas",
     statsVotes: "Votos",
     statsVotes: "Votos",
     statsMarket: "Mercado",
     statsMarket: "Mercado",
     statsForum: "Foros",
     statsForum: "Foros",
-    statsJob: "Trabajos",
-    statsReport: "Informes",
-    statsFeed: "Feeds",
+    statsJob: "Empleos",
+    statsProject: "Proyectos",
+    statsReport: "Reportes",
+    statsFeed: "Publicaciones",
     statsTribe: "Tribus",
     statsTribe: "Tribus",
     statsImage: "Imágenes",
     statsImage: "Imágenes",
     statsAudio: "Audios",
     statsAudio: "Audios",
-    statsVideo: "Videos",
+    statsVideo: "Vídeos",
     statsDocument: "Documentos",
     statsDocument: "Documentos",
     statsTransfer: "Transferencias",
     statsTransfer: "Transferencias",
     statsAiExchange: "IA",
     statsAiExchange: "IA",
@@ -1669,13 +1863,13 @@ module.exports = {
     statsOasisID: "ID de Oasis",
     statsOasisID: "ID de Oasis",
     statsSize: "Total (tamaño)",
     statsSize: "Total (tamaño)",
     statsBlockchainSize: "Blockchain (tamaño)",
     statsBlockchainSize: "Blockchain (tamaño)",
-    statsBlobsSize: "Blobs (tamaño)",   
+    statsBlobsSize: "Blobs (tamaño)",
     statsActivity7d: "Actividad (últimos 7 días)",
     statsActivity7d: "Actividad (últimos 7 días)",
-    statsActivity7dTotal: "Total de 7 días",
-    statsActivity30dTotal: "Total de 30 días",
-    statsKarmaScore: "Puntuación de KARMA",
-    statsPublic: "Públicas",
-    statsPrivate: "Privadas",
+    statsActivity7dTotal: "Total 7 días",
+    statsActivity30dTotal: "Total 30 días",
+    statsKarmaScore: "Puntuación KARMA",
+    statsPublic: "Público",
+    statsPrivate: "Privado",
     day: "Día",
     day: "Día",
     messages: "Mensajes",
     messages: "Mensajes",
     statsProject: "Proyectos",
     statsProject: "Proyectos",
@@ -1686,32 +1880,49 @@ module.exports = {
     statsProjectsPaused: "Pausados",
     statsProjectsPaused: "Pausados",
     statsProjectsCancelled: "Cancelados",
     statsProjectsCancelled: "Cancelados",
     statsProjectsGoalTotal: "Meta total",
     statsProjectsGoalTotal: "Meta total",
-    statsProjectsPledgedTotal: "Total comprometido",
+    statsProjectsPledgedTotal: "Aportación total",
     statsProjectsSuccessRate: "Tasa de éxito",
     statsProjectsSuccessRate: "Tasa de éxito",
-    statsProjectsAvgProgress: "Progreso promedio",
-    statsProjectsMedianProgress: "Progreso medio",
-    statsProjectsActiveFundingAvg: "Promedio de financiación activa",
-    statsJobsTitle: "Trabajos",
-    statsJobsTotal: "Total de trabajos",
+    statsProjectsAvgProgress: "Progreso medio",
+    statsProjectsMedianProgress: "Progreso mediano",
+    statsProjectsActiveFundingAvg: "Financiación activa media",
+    statsJobsTitle: "Empleos",
+    statsJobsTotal: "Total de empleos",
     statsJobsOpen: "Abiertos",
     statsJobsOpen: "Abiertos",
     statsJobsClosed: "Cerrados",
     statsJobsClosed: "Cerrados",
     statsJobsOpenVacants: "Vacantes abiertas",
     statsJobsOpenVacants: "Vacantes abiertas",
-    statsJobsSubscribersTotal: "Total de suscriptores",
-    statsJobsAvgSalary: "Salario promedio",
+    statsJobsSubscribersTotal: "Suscriptores totales",
+    statsJobsAvgSalary: "Salario medio",
     statsJobsMedianSalary: "Salario mediano",
     statsJobsMedianSalary: "Salario mediano",
     statsMarketTitle: "Mercado",
     statsMarketTitle: "Mercado",
-    statsMarketTotal: "Total de artículos",
+    statsMarketTotal: "Artículos totales",
     statsMarketForSale: "En venta",
     statsMarketForSale: "En venta",
     statsMarketReserved: "Reservados",
     statsMarketReserved: "Reservados",
     statsMarketClosed: "Cerrados",
     statsMarketClosed: "Cerrados",
     statsMarketSold: "Vendidos",
     statsMarketSold: "Vendidos",
     statsMarketRevenue: "Ingresos",
     statsMarketRevenue: "Ingresos",
-    statsMarketAvgSoldPrice: "Precio promedio de venta",
+    statsMarketAvgSoldPrice: "Precio medio vendido",
     statsUsersTitle: "Habitantes",
     statsUsersTitle: "Habitantes",
     user: "Habitante",
     user: "Habitante",
-    statsTombstoneTitle: "Tumbas",
-    statsNetworkTombstones: "Tumbas de la red",
-    statsTombstoneRatio: "Ratio de tumbas (%)",
+    statsTombstoneTitle: "Eliminados",
+    statsNetworkTombstones: "Eliminados en la red",
+    statsTombstoneRatio: "Ratio de eliminados (%)",
+    statsAITraining: "Entrenamiento de IA",
+    statsAIExchanges: "Intercambios",
+    bankingUserEngagementScore: "Puntuación de participación del usuario",
+    statsParliamentCandidature: "Candidaturas de parlamento",
+    statsParliamentTerm: "Mandatos de parlamento",
+    statsParliamentProposal: "Propuestas de parlamento",
+    statsParliamentRevocation: "Revocaciones de parlamento",
+    statsParliamentLaw: "Leyes de parlamento",
+    statsCourtsCase: "Casos judiciales",
+    statsCourtsEvidence: "Evidencias judiciales",
+    statsCourtsAnswer: "Respuestas judiciales",
+    statsCourtsVerdict: "Veredictos judiciales",
+    statsCourtsSettlement: "Acuerdos judiciales",
+    statsCourtsSettlementProposal: "Propuestas de acuerdo",
+    statsCourtsSettlementAccepted: "Acuerdos aceptados",
+    statsCourtsNomination: "Nominaciones de jueces",
+    statsCourtsNominationVote: "Votos de nominación",
     //AI
     //AI
     ai: "IA",
     ai: "IA",
     aiTitle: "IA",
     aiTitle: "IA",
@@ -2014,6 +2225,8 @@ module.exports = {
     modulesVotationsDescription: "Módulo para descubrir y gestionar votaciones.",
     modulesVotationsDescription: "Módulo para descubrir y gestionar votaciones.",
     modulesParliamentLabel: "Parlamento",
     modulesParliamentLabel: "Parlamento",
     modulesParliamentDescription: "Módulo para elegir gobiernos y votar leyes.",
     modulesParliamentDescription: "Módulo para elegir gobiernos y votar leyes.",
+    modulesCourtsLabel: "Juzgados",
+    modulesCourtsDescription: "Módulo para resolver conflictos y emitir veredictos.",
     modulesReportsLabel: "Informes",
     modulesReportsLabel: "Informes",
     modulesReportsDescription: "Módulo para gestionar y hacer un seguimiento de informes relacionados con problemas, errores, abusos y advertencias de contenido.",
     modulesReportsDescription: "Módulo para gestionar y hacer un seguimiento de informes relacionados con problemas, errores, abusos y advertencias de contenido.",
     modulesOpinionsLabel: "Opiniones",
     modulesOpinionsLabel: "Opiniones",

+ 278 - 112
src/client/assets/translations/oasis_eu.js

@@ -757,6 +757,121 @@ module.exports = {
     parliamentRulesLaws: "Proposamenak bere atalasea gainditzen duenean, Lege bihurtzen da eta Legeak fitxan agertzen da indarrean sartzeko datarekin.",
     parliamentRulesLaws: "Proposamenak bere atalasea gainditzen duenean, Lege bihurtzen da eta Legeak fitxan agertzen da indarrean sartzeko datarekin.",
     parliamentRulesHistorical: "Historia fitxan egondako gobernu ziklo bakoitza eta haren kudeaketari buruzko datuak ikus daitezke.",
     parliamentRulesHistorical: "Historia fitxan egondako gobernu ziklo bakoitza eta haren kudeaketari buruzko datuak ikus daitezke.",
     parliamentRulesLeaders: "Liderak fitxan, gobernatu duten (edo aurkeztu diren) biztanle/tribuen sailkapena ikus daiteke, eraginkortasunaren arabera ordenatuta.",
     parliamentRulesLeaders: "Liderak fitxan, gobernatu duten (edo aurkeztu diren) biztanle/tribuen sailkapena ikus daiteke, eraginkortasunaren arabera ordenatuta.",
+    //courts
+    courtsTitle: "Auzitegiak",
+    courtsDescription: "Aztertu gatazkak konpontzeko eta justizia kolektiboa kudeatzeko modu desberdinak.",
+    courtsFilterCases: "KASUAK",
+    courtsFilterMyCases: "NIREAK",
+    courtsFilterJudges: "EPAILEAK",
+    courtsFilterHistory: "HISTORIA",
+    courtsFilterRules: "ARAUAK",
+    courtsFilterOpenCase: "KASU BERRIA",
+    courtsCaseFormTitle: "Kasu ireki",
+    courtsCaseTitle: "Izenburua",
+    courtsCaseRespondent: "Akusatua / Erantzulea",
+    courtsCaseRespondentPh: "Oasis ID (@...) edo Tribu izena",
+    courtsCaseMediatorsAccuser: "Bitartekariak (akusazioa)",
+    courtsCaseMediatorsPh: "Oasis IDak, komaz bereizita",
+    courtsCaseMethod: "Ebazpen metodoa",
+    courtsCaseDescription: "Azalpena (gehienez 1000 karaktere)",
+    courtsCaseEvidenceTitle: "Kasuko frogak",
+    courtsCaseEvidenceHelp: "Erantsi zure kasua babesten duten irudiak, audioak, dokumentuak (PDF) edo bideoak.",
+    courtsCaseSubmit: "Kasu bidali",
+    courtsNominateJudge: "Epailea proposatu",
+    courtsJudgeId: "Epailea",
+    courtsJudgeIdPh: "Oasis ID (@...) edo Biztanle izena",
+    courtsNominateBtn: "Proposatu",
+    courtsAddEvidence: "Frogak gehitu",
+    courtsEvidenceText: "Testua",
+    courtsEvidenceLink: "Esteka",
+    courtsEvidenceLinkPh: "https://…",
+    courtsEvidenceSubmit: "Erantsi",
+    courtsAnswerTitle: "Erreklamazioari erantzun",
+    courtsAnswerText: "Erantzun laburra",
+    courtsAnswerSubmit: "Erantzuna bidali",
+    courtsStanceDENY: "Ukatu",
+    courtsStanceADMIT: "Onartu",
+    courtsStancePARTIAL: "Partziala",
+    courtsVerdictTitle: "Ebazpena eman",
+    courtsVerdictResult: "Emaitza",
+    courtsVerdictOrders: "Aginduak",
+    courtsVerdictOrdersPh: "Ekintzak, epeak, neurri zuzentzaileak...",
+    courtsIssueVerdict: "Ebazpena eman",
+    courtsMediationPropose: "Akordioa proposatu",
+    courtsSettlementText: "Baldintzak",
+    courtsSettlementProposeBtn: "Proposatu",
+    courtsNominationsTitle: "Epailetzako izendapenak",
+    courtsThJudge: "Epailea",
+    courtsThSupports: "Babesak",
+    courtsThDate: "Data",
+    courtsThVote: "Bozkatu",
+    courtsNoNominations: "Oraindik ez dago izendapenik.",
+    courtsAccuser: "Akusatzailea",
+    courtsRespondent: "Erantzulea",
+    courtsThStatus: "Egoera",
+    courtsThAnswerBy: "Erantzuteko epea",
+    courtsThEvidenceBy: "Frogak aurkezteko epea",
+    courtsThDecisionBy: "Ebazteko epea",
+    courtsThCase: "Kasua",
+    courtsThCreatedAt: "Hasiera-data",
+    courtsThActions: "Ekintzak",
+    courtsCaseMediatorsRespondentTitle: "Defentsako bitartekariak gehitu",
+    courtsCaseMediatorsRespondent: "Bitartekariak (defentsa)",
+    courtsMediatorsAccuserLabel: "Bitartekariak (akusazioa)",
+    courtsMediatorsRespondentLabel: "Bitartekariak (defentsa)",
+    courtsMediatorsSubmit: "Bitartekariak gorde",
+    courtsVotesNeeded: "Beharrezko botoak",
+    courtsVotesSlashTotal: "BAI / GUZTIRA",
+    courtsOpenVote: "Bozketa ireki",
+    courtsPublicPrefLabel: "Ebazpenaren ondorengo ikusgarritasuna",
+    courtsPublicPrefYes: "Ados nago kasu hau guztiz publikoa izatearekin",
+    courtsPublicPrefNo: "Hobe dut xehetasunak pribatuan mantentzea",
+    courtsPublicPrefSubmit: "Ikusgarritasun lehentasuna gorde",
+    courtsNoCases: "Kasurik ez.",
+    courtsNoMyCases: "Oraindik ez duzu gatazkarik.",
+    courtsNoHistory: "Oraindik ez dago auzirik erregistratuta.",
+    courtsMethodJUDGE: "Epailea",
+    courtsMethodDICTATOR: "Diktadorea",
+    courtsMethodPOPULAR: "Herritarren bozketa",
+    courtsMethodMEDIATION: "Bitartekaritza",
+    courtsMethodKARMATOCRACY: "Karmatokrazia",
+    courtsMethod: "Metodoa",
+    courtsRulesTitle: "Nola funtzionatzen duten Auzitegiek",
+    courtsRulesIntro: "Auzitegiak komunitateak kudeatutako prozesuak dira, gatazkak konpontzeko eta justizia zuzentzailea sustatzeko. Elkarrizketa, froga argiak eta neurri proportzionalak lehenesten dira.",
+    courtsRulesLifecycle: "Prozesua: 1) Kasua ireki  2) Metodoa aukeratu  3) Frogak aurkeztu  4) Entzunaldia eta eztabaida  5) Epai eta neurriak  6) Betetze eta itxiera  7) Helegitea (badagokio).",
+    courtsRulesRoles: "Akusatzailea: kasua irekitzen du. Defentsa: akusatutako pertsona edo tribua. Metodoa: komunitateak aukeratutako mekanismoa, froga baloratu eta ebazpena emateko. Lekukoa: testigantza edo frogak ematen dituen pertsona. Bitartekariak: akusatzaileak eta/edo defentsak gonbidatutako pertsona neutralak, xehetasun guztietara sarbidea dutenak, gatazka deseskalatzen eta akordioak lankidetzan eraikitzen laguntzen dutenak.",
+    courtsRulesEvidence: "Gehienez 1000 karaktereko azalpena. Erantsi irudi, audio, bideo eta PDF dokumentu garrantzitsu eta legezkoak. Ez partekatu datu pribatu sentikorrak baimenik gabe.",
+    courtsRulesDeliberation: "Entzunaldiak publikoak edo pribatuak izan daitezke. Epaileek errespetua bermatzen dute, argipenak eskatzen dituzte eta material ez-legala edo esanguragabea baztertu dezakete.",
+    courtsRulesVerdict: "Lehentasuna da konponketa: barkamenak, bitartekaritza-akordioak, edukiaren moderazioa, behin-behineko murrizketak edo beste neurri proportzional batzuk. Arrazoibidea erregistratuta geratu behar da.",
+    courtsRulesAppeals: "Helegitea: onartzen da frogak berriak direnean edo akats prozesal argia dagoenean. Gehienez 7 eguneko epean aurkeztu behar da, bestela adierazi ezean.",
+    courtsRulesPrivacy: "Errespetatu pribatutasuna eta segurtasuna. Doxing-a, gorrotoa edo mehatxuak ezabatzen dira. Epaileek erregistroaren zatiak editatu edo ezkutatu ditzakete arriskuan dauden pertsonak babesteko.",
+    courtsRulesMisconduct: "Jazarpenak, manipulazioak edo faltsututako frogek berehalako ebazpen negatiboa ekar dezakete.",
+    courtsRulesGlossary: "Kasua: gatazkaren erregistroa. Froga: aldarrikapenak babesten dituzten materialak. Epai/ebazpena: neurri edo konponketarekin datorren erabakia. Helegitea: ebazpena berrikusteko eskaera.",
+    courtsFilterActions: "EKINTZAK",
+    courtsNoActions: "Ez duzu zain dauden ekintzarik zure rolarentzat.",
+    courtsCaseTitlePlaceholder: "Gatazkaren deskribapen laburra",
+    courtsCaseSeverity: "Larritasuna",
+    courtsCaseSeverityNone: "Larritasun etiketarik ez",
+    courtsCaseSeverityLOW: "Baxua",
+    courtsCaseSeverityMEDIUM: "Ertaina",
+    courtsCaseSeverityHIGH: "Handia",
+    courtsCaseSeverityCRITICAL: "Larria",
+    courtsCaseSubject: "Gaia",
+    courtsCaseSubjectNone: "Gairik gabeko etiketa",
+    courtsCaseSubjectBEHAVIOUR: "Jokabidea",
+    courtsCaseSubjectCONTENT: "Edukia",
+    courtsCaseSubjectGOVERNANCE: "Gobernantza / arauak",
+    courtsCaseSubjectFINANCIAL: "Finantzak / baliabideak",
+    courtsCaseSubjectOTHER: "Bestelakoa",
+    courtsHiddenRespondent: "Ezkutatua (rol inplikatuek bakarrik ikus dezakete).",
+    courtsThRole: "Rola",
+    courtsRoleAccuser: "Akusatzailea",
+    courtsRoleDefence: "Defentsa",
+    courtsRoleMediator: "Bitartekaria",
+    courtsRoleJudge: "Epailea",
+    courtsRoleDictator: "Diktadorea",
+    courtsAssignJudgeTitle: "Epailea aukeratu",
+    courtsAssignJudgeBtn: "Epailea aukeratu",
     //trending
     //trending
     trendingTitle: "Pil-pilean",
     trendingTitle: "Pil-pilean",
     exploreTrending: "Aurkitu pil-pileko edukia zure sarean.",
     exploreTrending: "Aurkitu pil-pileko edukia zure sarean.",
@@ -1179,56 +1294,76 @@ module.exports = {
     globalActivity:       "Jarduera globala",
     globalActivity:       "Jarduera globala",
     activityList:         "Jarduera",
     activityList:         "Jarduera",
     activityDesc:         "Ikusi zure sareko azken jarduera.",
     activityDesc:         "Ikusi zure sareko azken jarduera.",
-    allButton:            "GUZTIAK",
-    mineButton:           "NIREA",
-    noActions:            "Ez dago jarduerarik.",
+    allButton:            "GUZTIA",
+    mineButton:           "NIREAK",
+    noActions:            "Ez dago jarduerarik eskuragarri.",
     performed:            "→",
     performed:            "→",
     from:                 "Nork",
     from:                 "Nork",
     to:                   "Nori",
     to:                   "Nori",
     amount:               "Kopurua",
     amount:               "Kopurua",
     concept:              "Kontzeptua",
     concept:              "Kontzeptua",
     description:          "Deskribapena",
     description:          "Deskribapena",
-    meme:                 "Memea",
+    meme:                 "Meme",
     activityContact:      "Kontaktua",
     activityContact:      "Kontaktua",
     activityBy:           "Izena",
     activityBy:           "Izena",
     activityPixelia:      "Pixel berria gehituta",
     activityPixelia:      "Pixel berria gehituta",
-    viewImage:            "Irudia ikusi",
+    viewImage:            "Ikusi irudia",
     playAudio:            "Audioa erreproduzitu",
     playAudio:            "Audioa erreproduzitu",
     playVideo:            "Bideoa erreproduzitu",
     playVideo:            "Bideoa erreproduzitu",
-    typeRecent:           "AZKENAK",
+    typeRecent:           "AZKENA",
     errorActivity:        "Errorea jarduera eskuratzean",
     errorActivity:        "Errorea jarduera eskuratzean",
     typePost:             "ARGITALPENA",
     typePost:             "ARGITALPENA",
-    typeTribe:            "TRIBUA",
-    typeAbout:            "BIZTANLEA",
+    typeTribe:            "TRIBUAK",
+    typeAbout:            "BIZTANLEAK",
     typeCurriculum:       "CV",
     typeCurriculum:       "CV",
-    typeImage:            "IRUDIA",
-    typeBookmark:         "LASTER-MARKA",
-    typeDocument:         "DOKUMENTUA",
-    typeVotes:            "GOBERNANTZA",
-    typeAudio:            "AUDIOA",
+    typeImage:            "IRUDIAK",
+    typeBookmark:         "LASTER-MARKAK",
+    typeDocument:         "DOKUMENTUAK",
+    typeVotes:            "BOZKETAK",
+    typeAudio:            "AUDIOAK",
     typeMarket:           "MERKATUA",
     typeMarket:           "MERKATUA",
-    typeJob:              "LANA",
-    typeProject:          "PROIEKTUA",
-    typeVideo:            "BIDEOA",
+    typeJob:              "LANAK",
+    typeProject:          "PROIEKTUAK",
+    typeVideo:            "BIDEOAK",
     typeVote:             "ZABALKUNDEA",
     typeVote:             "ZABALKUNDEA",
-    typeEvent:            "GERTAERA",
+    typeEvent:            "EKITALDIAK",
     typeTransfer:         "TRANSFERENTZIA",
     typeTransfer:         "TRANSFERENTZIA",
-    typeTask:             "ZEREGINAK",
+    typeTask:             "ATASKAK",
     typePixelia:          "PIXELIA",
     typePixelia:          "PIXELIA",
     typeForum:            "FOROA",
     typeForum:            "FOROA",
-    typeReport:           "TXOSTENA",
-    typeFeed:             "JARIOA",
+    typeReport:           "TXOSTENAK",
+    typeFeed:             "FEEDA",
     typeContact:          "KONTAKTUA",
     typeContact:          "KONTAKTUA",
     typePub:              "PUB",
     typePub:              "PUB",
     typeTombstone:        "TOMBSTONE",
     typeTombstone:        "TOMBSTONE",
     typeBanking:          "BANKUA",
     typeBanking:          "BANKUA",
+    typeBankWallet:       "BANKUA/ZORROA",
+    typeBankClaim:        "BANKUA/UBI",
+    typeKarmaScore:       "KARMA",
+    typeParliament:       "PARLAMENTUA",
+    typeParliamentCandidature: "Parlamentua · Hautagaitza",
+    typeParliamentTerm:   "Parlamentua · Agintaldia",
+    typeParliamentProposal:"Parlamentua · Proposamena",
+    typeParliamentRevocation:"Parlamentua · Ezespena",
+    typeParliamentLaw:    "Parlamentua · Lege berria",
+    typeCourts:           "EPAITEGIAK",
+    typeCourtsCase:       "Epaitegiak · Kausa",
+    typeCourtsEvidence:   "Epaitegiak · Froga",
+    typeCourtsAnswer:     "Epaitegiak · Erantzuna",
+    typeCourtsVerdict:    "Epaitegiak · Epai",
+    typeCourtsSettlement: "Epaitegiak · Akordioa",
+    typeCourtsSettlementProposal: "Epaitegiak · Akordio proposamena",
+    typeCourtsSettlementAccepted: "Epaitegiak · Akordio onartua",
+    typeCourtsNomination: "Epaitegiak · Izendapena",
+    typeCourtsNominationVote: "Epaitegiak · Izendapen bozketak",
     activitySupport:      "Aliantza berria sortua",
     activitySupport:      "Aliantza berria sortua",
-    activityJoin:         "PUB berri bat batu da",
+    activityJoin:         "PUB berria batu da",
     question:             "Galdera",
     question:             "Galdera",
-    deadline:             "Epea",
+    deadline:             "Epemuga",
     status:               "Egoera",
     status:               "Egoera",
     votes:                "Botoak",
     votes:                "Botoak",
-    totalVotes:           "Boto guztira",
+    totalVotes:           "Guztira botoak",
+    voteTotalVotes:       "Guztira botoak",
     name:                 "Izena",
     name:                 "Izena",
     skills:               "Gaitasunak",
     skills:               "Gaitasunak",
     tags:                 "Etiketak",
     tags:                 "Etiketak",
@@ -1237,24 +1372,49 @@ module.exports = {
     category:             "Kategoria",
     category:             "Kategoria",
     attendees:            "Parte-hartzaileak",
     attendees:            "Parte-hartzaileak",
     activitySpread:       "->",
     activitySpread:       "->",
-    visitLink:            "Esteka ikusi",
+    visitLink:            "Esteka bisitatu",
     viewDocument:         "Dokumentua ikusi",
     viewDocument:         "Dokumentua ikusi",
     location:             "Kokalekua",
     location:             "Kokalekua",
     contentWarning:       "Gaia",
     contentWarning:       "Gaia",
     personName:           "Biztanlearen izena",
     personName:           "Biztanlearen izena",
-    typeBankWallet:       "BANKUA/ZORROA",
-    typeBankClaim:        "BANKUA/UBI",
-    typeKarmaScore:	  "KARMA",
-    bankWalletConnected:  "ECOin Zorroa",
-    bankUbiReceived:      "UBI jasota",
+    bankWalletConnected:  "ECOin zorroa",
+    bankUbiReceived:      "Jasotako UBI",
     bankTx:               "Tx",
     bankTx:               "Tx",
     bankEpochShort:       "Epoka",
     bankEpochShort:       "Epoka",
-    activityProjectFollow: "%OASIS% orain proiektu hau %ACTION% ari da %PROJECT%",
-    activityProjectUnfollow: "%OASIS% orain proiektu hau %ACTION% ari da %PROJECT%",
-    activityProjectPledged: "%OASIS%(e)k %AMOUNT% %ACTION% du proiektuan %PROJECT%",
-    following: "JARRAITZEN",
-    unfollowing: "UTZI DU JARRAITZEA",
-    pledged: "KONPROMISOA",
+    bankAllocId:          "Esleipen IDa",
+    bankingUserEngagementScore: "Erabiltzaile inplikazio puntuazioa",
+    viewDetails:          "Xehetasunak ikusi",
+    link:                 "Esteka",
+    aiSnippetsLearned:    "Ikasitako zatiak",
+    tribeFeedRefeeds:     "Birbidalketak",
+    activityProjectFollow:   "%OASIS% orain %ACTION% proiektu hau %PROJECT%",
+    activityProjectUnfollow: "%OASIS% orain %ACTION% proiektu hau %PROJECT%",
+    activityProjectPledged:  "%OASIS%-(e)k %ACTION% %AMOUNT% egin du %PROJECT% proiektuan",
+    following:            "JARRAITZEN",
+    unfollowing:          "EZ JARRAITZEN",
+    pledged:              "KONPROMISOA",
+    parliamentCandidatureId: "Kandidatura",
+    parliamentGovMethod:     "Metodoa",
+    parliamentVotesReceived: "Jasotako botoak",
+    parliamentMethodANARCHY:   "Anarkia",
+    parliamentMethodVOTE:      "Komunitate-bozketa",
+    parliamentMethodRANKED:    "Hobespen-bozketa",
+    parliamentMethodPLURALITY: "Pluralitatea",
+    parliamentMethodCOUNCIL:   "Kontseilua",
+    parliamentMethodJURY:      "Epaimahaia",
+    parliamentAnarchy:         "ANARKIA",
+    parliamentElectionsStart:  "Hauteskundeen hasiera",
+    parliamentElectionsEnd:    "Hauteskundeen amaiera",
+    parliamentCurrentLeader:   "Irabazleko kandidatura",
+    parliamentProposalTitle:   "Izenburua",
+    parliamentOpenVote:        "Bozketa irekia",
+    parliamentStatus:          "Egoera",
+    parliamentLawQuestion:     "Galdera",
+    parliamentLawMethod:       "Metodoa",
+    parliamentLawProposer:     "Proposatzailea",
+    parliamentLawEnacted:      "Onartutako eguna",
+    parliamentLawVotes:        "Botoak",
+    createdAt:                 "Sortze-data",
     //reports
     //reports
     reportsTitle: "Txostenak",
     reportsTitle: "Txostenak",
     reportsDescription: "Kudeatu eta jarraitu arazo, akats, gehiegikeri eta eduki-abisuei buruzko txostena zure sarean.",
     reportsDescription: "Kudeatu eta jarraitu arazo, akats, gehiegikeri eta eduki-abisuei buruzko txostena zure sarean.",
@@ -1603,17 +1763,17 @@ module.exports = {
     //stats
     //stats
     statsTitle: 'Estatistikak',
     statsTitle: 'Estatistikak',
     statistics: "Estatistikak",
     statistics: "Estatistikak",
-    statsDescription: "Aurkitu zure sareari buruzko estatistikak.",
+    statsInhabitant: "Bizilagunaren estatistikak",
+    statsDescription: "Ezagutu zure sareari buruzko estatistikak.",
     ALLButton: "GUZTIAK",
     ALLButton: "GUZTIAK",
-    MINEButton: "NEUREAK",
-    VISUALButton: "BISUALA",
-    TOMBSTONEButton: "HILARRIAK",
+    MINEButton: "NIREAK",
+    TOMBSTONEButton: "EZABAKETAK",
     statsYou: "Zu",
     statsYou: "Zu",
-    statsUserId: "Oasis ID-a",
-    statsCreatedAt: "Noiz",
+    statsUserId: "Oasis ID",
+    statsCreatedAt: "Sortze data",
     statsYourContent: "Edukia",
     statsYourContent: "Edukia",
     statsYourOpinions: "Iritziak",
     statsYourOpinions: "Iritziak",
-    statsYourTombstone: "Hilarriak",
+    statsYourTombstone: "Ezabaketak",
     statsNetwork: "Sarea",
     statsNetwork: "Sarea",
     statsTotalInhabitants: "Bizilagunak",
     statsTotalInhabitants: "Bizilagunak",
     statsDiscoveredTribes: "Tribuak (Publikoak)",
     statsDiscoveredTribes: "Tribuak (Publikoak)",
@@ -1621,98 +1781,102 @@ module.exports = {
     statsNetworkContent: "Edukia",
     statsNetworkContent: "Edukia",
     statsYourMarket: "Merkatua",
     statsYourMarket: "Merkatua",
     statsYourJob: "Lanak",
     statsYourJob: "Lanak",
+    statsYourProject: "Proiektuak",
     statsYourTransfer: "Transferentziak",
     statsYourTransfer: "Transferentziak",
     statsYourForum: "Foroak",   
     statsYourForum: "Foroak",   
-    statsYourProject: "Proiektuak",
-    statsDiscoveredProject: "Proiektuak",
-    statsBankingTitle: "Banking",
-    statsEcoWalletLabel: "ECOIN Zorroa",
-    statsEcoWalletNotConfigured: "Konfiguratu gabe!",
-    statsTotalEcoAddresses: "Guztira helbideak",
-    statsProject: "Proiektuak",
-    statsProject: "Proiektuak",
-    statsProjectsTitle: "Proiektuen",
-    statsProjectsTotal: "Proiektu guztira",
-    statsProjectsActive: "Aktiboak",
-    statsProjectsCompleted: "Osatuak",
-    statsProjectsPaused: "Pausatuta",
-    statsProjectsCancelled: "Ezeztatuta",
-    statsProjectsGoalTotal: "Helburu guztira",
-    statsProjectsPledgedTotal: "Konprometitutako guztira",
-    statsProjectsSuccessRate: "Arrakasta tasa",
-    statsProjectsAvgProgress: "Progresu ertaina",
-    statsProjectsMedianProgress: "Progresu medianoa",
-    statsProjectsActiveFundingAvg: "Aktibo finantzatzeko ertaina",
-    statsNetworkOpinions:  "Iritziak",
+    statsNetworkOpinions: "Iritziak",
     statsDiscoveredMarket: "Merkatua",
     statsDiscoveredMarket: "Merkatua",
-    statsDiscoveredJobs:   "Lanak",
+    statsDiscoveredJob: "Lanak",
+    statsDiscoveredProject: "Proiektuak",
+    statsBankingTitle: "Bankua",
+    statsEcoWalletLabel: "ECOIN zorroa",
+    statsEcoWalletNotConfigured:  "Konfiguratu gabe!",
+    statsTotalEcoAddresses: "Helbide kopurua",
     statsDiscoveredTransfer: "Transferentziak",
     statsDiscoveredTransfer: "Transferentziak",
     statsDiscoveredForum: "Foroak",
     statsDiscoveredForum: "Foroak",
-    statsNetworkTombstone: "Hilarriak",
-    statsBookmarks: "Markagailuak",
-    statsEvents: "Ekitaldiak",
-    statsTasks: "Atazak",
-    statsVotes: "Bozkak",
+    statsNetworkTombstone: "Ezabaketak",
+    statsBookmark: "Laster-markak",
+    statsEvent: "Ekitaldiak",
+    statsTask: "Zereginak",
+    statsVotes: "Botoak",
     statsMarket: "Merkatua",
     statsMarket: "Merkatua",
     statsForum: "Foroak",
     statsForum: "Foroak",
     statsJob: "Lanak",
     statsJob: "Lanak",
-    statsReports: "Txostenak",
-    statsFeeds: "Jarioak",
-    statsTribes: "Tribuak",
-    statsImages: "Irudiak",
-    statsAudios: "Audioak",
-    statsVideos: "Bideoak",
-    statsDocuments: "Dokumentuak",
-    statsTransfers: "Transferentziak",
+    statsProject: "Proiektuak",
+    statsReport: "Txostenak",
+    statsFeed: "Jarioak",
+    statsTribe: "Tribuak",
+    statsImage: "Irudiak",
+    statsAudio: "Audioak",
+    statsVideo: "Bideoak",
+    statsDocument: "Dokumentuak",
+    statsTransfer: "Transferentziak",
     statsAiExchange: "IA",
     statsAiExchange: "IA",
-    statsPUBs: 'PUBs',
-    statsPosts: "Bidalketak",
-    statsOasisID: "Oasis ID-a",
-    statsSize: "Guztira (taimaina)",
+    statsPUBs: 'PUBack',
+    statsPost: "Mezuak",
+    statsOasisID: "Oasis ID",
+    statsSize: "Guztira (tamaina)",
     statsBlockchainSize: "Blockchain (tamaina)",
     statsBlockchainSize: "Blockchain (tamaina)",
-    statsBlobsSize: "Blob-ak (tamaina)",
-    statsActivity7d: "Aktibitatea (azken 7 egunetan)",
-    statsActivity7dTotal: "7 egunerako guztira",
-    statsActivity30dTotal: "30 egunerako guztira",
-    statsKarmaScore: "KARMA Puntuazioa",
+    statsBlobsSize: "Blobak (tamaina)",
+    statsActivity7d: "Jarduera (azken 7 egun)",
+    statsActivity7dTotal: "7 eguneko guztira",
+    statsActivity30dTotal: "30 eguneko guztira",
+    statsKarmaScore: "KARMA puntuazioa",
     statsPublic: "Publikoa",
     statsPublic: "Publikoa",
     statsPrivate: "Pribatua",
     statsPrivate: "Pribatua",
     day: "Eguna",
     day: "Eguna",
     messages: "Mezuak",
     messages: "Mezuak",
     statsProject: "Proiektuak",
     statsProject: "Proiektuak",
-    statsProjectsTitle: "Proiektuen",
-    statsProjectsTotal: "Proiektu guztira",
-    statsProjectsActive: "Aktiboak",
-    statsProjectsCompleted: "Osatuak",
+    statsProjectsTitle: "Proiektuak",
+    statsProjectsTotal: "Proiektu kopurua",
+    statsProjectsActive: "Aktibo",
+    statsProjectsCompleted: "Amaituak",
     statsProjectsPaused: "Pausatuta",
     statsProjectsPaused: "Pausatuta",
-    statsProjectsCancelled: "Ezeztatuta",
-    statsProjectsGoalTotal: "Helburu guztira",
+    statsProjectsCancelled: "Bertan behera",
+    statsProjectsGoalTotal: "Helburu osoa",
     statsProjectsPledgedTotal: "Konprometitutako guztira",
     statsProjectsPledgedTotal: "Konprometitutako guztira",
     statsProjectsSuccessRate: "Arrakasta tasa",
     statsProjectsSuccessRate: "Arrakasta tasa",
-    statsProjectsAvgProgress: "Progresu ertaina",
-    statsProjectsMedianProgress: "Progresu medianoa",
-    statsProjectsActiveFundingAvg: "Aktibo finantzatzeko ertaina",
-    statsJobsTitle: "Lan",
-    statsJobsTotal: "Lan guztira",
-    statsJobsOpen: "Irekiak",
+    statsProjectsAvgProgress: "Batez besteko aurrerapena",
+    statsProjectsMedianProgress: "Aurrerapen mediana",
+    statsProjectsActiveFundingAvg: "Finantzaketa aktiboaren batez bestekoa",
+    statsJobsTitle: "Lanak",
+    statsJobsTotal: "Lan kopurua",
+    statsJobsOpen: "Irekita",
     statsJobsClosed: "Itxita",
     statsJobsClosed: "Itxita",
-    statsJobsOpenVacants: "Hutsik dauden lanak",
-    statsJobsSubscribersTotal: "Suskribatzaile guztira",
+    statsJobsOpenVacants: "Plaza irekiak",
+    statsJobsSubscribersTotal: "Harpidedun kopurua",
     statsJobsAvgSalary: "Soldata ertaina",
     statsJobsAvgSalary: "Soldata ertaina",
-    statsJobsMedianSalary: "Soldata medianoa",
-    statsMarketTitle: "Merkatu",
-    statsMarketTotal: "Artikulu guztira",
-    statsMarketForSale: "Salmentan",
+    statsJobsMedianSalary: "Soldata mediana",
+    statsMarketTitle: "Merkatua",
+    statsMarketTotal: "Gai kopurua",
+    statsMarketForSale: "Salgai",
     statsMarketReserved: "Erreserbatuta",
     statsMarketReserved: "Erreserbatuta",
     statsMarketClosed: "Itxita",
     statsMarketClosed: "Itxita",
-    statsMarketSold: "Saltzen",
-    statsMarketRevenue: "Sarrera",
-    statsMarketAvgSoldPrice: "Salmenta bateko prezio ertaina",
-    statsUsersTitle: "Biztanleak",
-    user: "Biztanlea",
-    statsTombstoneTitle: "Hilerriak",
-    statsNetworkTombstones: "Sareko hilerriak",
-    statsTombstoneRatio: "Hilerri ratioa (%)",
+    statsMarketSold: "Salduta",
+    statsMarketRevenue: "Diru-sarrerak",
+    statsMarketAvgSoldPrice: "Salmenta prezio ertaina",
+    statsUsersTitle: "Bizilagunak",
+    user: "Bizilaguna",
+    statsTombstoneTitle: "Ezabaketak",
+    statsNetworkTombstones: "Sareko ezabaketak",
+    statsTombstoneRatio: "Ezabaketa ratioa (%)",
+    statsAITraining: "IA prestakuntza",
+    statsAIExchanges: "Trukeak",
+    bankingUserEngagementScore: "Erabiltzaile inplikazioaren puntuazioa",
+    statsParliamentCandidature: "Parlamenturako hautagaitzak",
+    statsParliamentTerm: "Parlamentuko agintaldiak",
+    statsParliamentProposal: "Parlamentuko proposamenak",
+    statsParliamentRevocation: "Parlamentuko ezeztapenak",
+    statsParliamentLaw: "Parlamentuko legeak",
+    statsCourtsCase: "Epaitegietako kasuak",
+    statsCourtsEvidence: "Frogak",
+    statsCourtsAnswer: "Erantzunak",
+    statsCourtsVerdict: "Epaia",
+    statsCourtsSettlement: "Akordioak",
+    statsCourtsSettlementProposal: "Akordio-proposamenak",
+    statsCourtsSettlementAccepted: "Onartutako akordioak",
+    statsCourtsNomination: "Epaileen izendapenak",
+    statsCourtsNominationVote: "Izendapenen botoak",
     //IA
     //IA
     ai: "IA",
     ai: "IA",
     aiTitle: "IA",
     aiTitle: "IA",
@@ -2013,6 +2177,8 @@ module.exports = {
     modulesVotationsDescription: "Bozketak aurkitu eta kudeatzeko modulua.", 
     modulesVotationsDescription: "Bozketak aurkitu eta kudeatzeko modulua.", 
     modulesParliamentLabel: "Legebiltzarra",
     modulesParliamentLabel: "Legebiltzarra",
     modulesParliamentDescription: "Gobernuak hautatu eta legeak bozkatzeko modulua.",
     modulesParliamentDescription: "Gobernuak hautatu eta legeak bozkatzeko modulua.",
+    modulesCourtsLabel: "Epaitegiak",
+    modulesCourtsDescription: "Gatazkak konpontzeko eta epaia emateko modulua.",
     modulesReportsLabel: "Txostenak",
     modulesReportsLabel: "Txostenak",
     modulesReportsDescription: "Arazo, akats, abusu eta eduki-abisuetan erlazionatutako txostenak kudeatzeko modulua.",
     modulesReportsDescription: "Arazo, akats, abusu eta eduki-abisuetan erlazionatutako txostenak kudeatzeko modulua.",
     modulesOpinionsLabel: "Iritziak",
     modulesOpinionsLabel: "Iritziak",

+ 290 - 77
src/client/assets/translations/oasis_fr.js

@@ -756,6 +756,121 @@ module.exports = {
     parliamentRulesLaws: "Lorsqu’une proposition atteint son seuil, elle devient une Loi et apparaît dans l’onglet Lois avec sa date d’entrée en vigueur.",
     parliamentRulesLaws: "Lorsqu’une proposition atteint son seuil, elle devient une Loi et apparaît dans l’onglet Lois avec sa date d’entrée en vigueur.",
     parliamentRulesHistorical: "Dans l’onglet Historique, vous pouvez voir chaque cycle de gouvernement qui a eu lieu ainsi que des données sur sa gestion.",
     parliamentRulesHistorical: "Dans l’onglet Historique, vous pouvez voir chaque cycle de gouvernement qui a eu lieu ainsi que des données sur sa gestion.",
     parliamentRulesLeaders: "Dans l’onglet Dirigeants, vous pouvez voir un classement des habitants/tribus qui ont gouverné (ou se sont présentés), classés par efficacité.",
     parliamentRulesLeaders: "Dans l’onglet Dirigeants, vous pouvez voir un classement des habitants/tribus qui ont gouverné (ou se sont présentés), classés par efficacité.",
+    //courts
+    courtsTitle: "Tribunaux",
+    courtsDescription: "Explorez des formes de résolution des conflits et de gestion collective de la justice.",
+    courtsFilterCases: "AFFAIRES",
+    courtsFilterMyCases: "MIENNES",
+    courtsFilterJudges: "JUGES",
+    courtsFilterHistory: "HISTORIQUE",
+    courtsFilterRules: "RÈGLES",
+    courtsFilterOpenCase: "NOUVELLE AFFAIRE",
+    courtsCaseFormTitle: "Ouvrir une affaire",
+    courtsCaseTitle: "Titre",
+    courtsCaseRespondent: "Accusé / Défendeur",
+    courtsCaseRespondentPh: "ID Oasis (@...) ou nom de Tribu",
+    courtsCaseMediatorsAccuser: "Médiateurs (accusation)",
+    courtsCaseMediatorsPh: "ID Oasis, séparés par des virgules",
+    courtsCaseMethod: "Méthode de résolution",
+    courtsCaseDescription: "Description (1000 caractères max)",
+    courtsCaseEvidenceTitle: "Preuves de l'affaire",
+    courtsCaseEvidenceHelp: "Joignez des images, audios, documents (PDF) ou vidéos qui appuient votre affaire.",
+    courtsCaseSubmit: "Déposer l'affaire",
+    courtsNominateJudge: "Nommer un juge",
+    courtsJudgeId: "Juge",
+    courtsJudgeIdPh: "ID Oasis (@...) ou nom d'Habitant",
+    courtsNominateBtn: "Nommer",
+    courtsAddEvidence: "Ajouter des preuves",
+    courtsEvidenceText: "Texte",
+    courtsEvidenceLink: "Lien",
+    courtsEvidenceLinkPh: "https://…",
+    courtsEvidenceSubmit: "Joindre",
+    courtsAnswerTitle: "Répondre à la plainte",
+    courtsAnswerText: "Résumé de la réponse",
+    courtsAnswerSubmit: "Envoyer la réponse",
+    courtsStanceDENY: "Refuser",
+    courtsStanceADMIT: "Admettre",
+    courtsStancePARTIAL: "Partielle",
+    courtsVerdictTitle: "Rendre un verdict",
+    courtsVerdictResult: "Résultat",
+    courtsVerdictOrders: "Ordres",
+    courtsVerdictOrdersPh: "Actions, délais, mesures réparatrices...",
+    courtsIssueVerdict: "Rendre le verdict",
+    courtsMediationPropose: "Proposer un accord",
+    courtsSettlementText: "Termes",
+    courtsSettlementProposeBtn: "Proposer",
+    courtsNominationsTitle: "Nominations à la magistrature",
+    courtsThJudge: "Juge",
+    courtsThSupports: "Soutiens",
+    courtsThDate: "Date",
+    courtsThVote: "Vote",
+    courtsNoNominations: "Aucune nomination pour l’instant.",
+    courtsAccuser: "Accusation",
+    courtsRespondent: "Défense",
+    courtsThStatus: "Statut",
+    courtsThAnswerBy: "Répondre avant le",
+    courtsThEvidenceBy: "Preuves avant le",
+    courtsThDecisionBy: "Décision avant le",
+    courtsThCase: "Affaire",
+    courtsThCreatedAt: "Date de début",
+    courtsThActions: "Actions",
+    courtsCaseMediatorsRespondentTitle: "Ajouter des médiateurs de la défense",
+    courtsCaseMediatorsRespondent: "Médiateurs (défense)",
+    courtsMediatorsAccuserLabel: "Médiateurs (accusation)",
+    courtsMediatorsRespondentLabel: "Médiateurs (défense)",
+    courtsMediatorsSubmit: "Enregistrer les médiateurs",
+    courtsVotesNeeded: "Voix nécessaires",
+    courtsVotesSlashTotal: "OUI / TOTAL",
+    courtsOpenVote: "Ouvrir le vote",
+    courtsPublicPrefLabel: "Visibilité après la résolution",
+    courtsPublicPrefYes: "J’accepte que cette affaire soit entièrement publique",
+    courtsPublicPrefNo: "Je préfère garder les détails privés",
+    courtsPublicPrefSubmit: "Enregistrer la préférence de visibilité",
+    courtsNoCases: "Aucune affaire.",
+    courtsNoMyCases: "Vous n’avez pas encore de conflits.",
+    courtsNoHistory: "Aucun procès enregistré pour l’instant.",
+    courtsMethodJUDGE: "Juge",
+    courtsMethodDICTATOR: "Dictateur",
+    courtsMethodPOPULAR: "Populaire",
+    courtsMethodMEDIATION: "Médiation",
+    courtsMethodKARMATOCRACY: "Karmatocratie",
+    courtsMethod: "Méthode",
+    courtsRulesTitle: "Fonctionnement des Tribunaux",
+    courtsRulesIntro: "Les Tribunaux sont un processus géré par la communauté pour résoudre les conflits et promouvoir la justice réparatrice. Le dialogue, des preuves claires et des remèdes proportionnels sont privilégiés.",
+    courtsRulesLifecycle: "Processus : 1) Ouvrir une affaire  2) Choisir une méthode  3) Soumettre des preuves  4) Audience et délibération  5) Verdict et remède  6) Exécution et clôture  7) Appel (le cas échéant).",
+    courtsRulesRoles: "Accusation : ouvre l’affaire. Défense : personne ou tribu mise en cause. Méthode : mécanisme choisi par la communauté pour faciliter, évaluer les preuves et rendre un verdict. Témoin : fournit des témoignages ou des preuves. Médiateurs : personnes neutres invitées par l’accusation et/ou la défense, avec accès à tous les détails, qui aident à désamorcer le conflit et à co-créer des accords.",
+    courtsRulesEvidence: "Description jusqu’à 1000 caractères. Joignez des images, audios, vidéos et documents PDF pertinents et légaux. Ne partagez pas de données privées sensibles sans consentement.",
+    courtsRulesDeliberation: "Les audiences peuvent être publiques ou privées. Les juges garantissent le respect, demandent des clarifications et peuvent écarter le matériel non pertinent ou illégal.",
+    courtsRulesVerdict: "La restauration est prioritaire : excuses, accords de médiation, modération de contenu, restrictions temporaires ou autres mesures proportionnées. Le raisonnement doit être consigné.",
+    courtsRulesAppeals: "Appel : autorisé en présence de nouvelles preuves ou d’une erreur procédurale manifeste. Doit être déposé dans les 7 jours sauf indication contraire.",
+    courtsRulesPrivacy: "Respectez la vie privée et la sécurité. Le doxing, la haine ou les menaces sont supprimés. Les juges peuvent modifier ou sceller certaines parties du dossier pour protéger les personnes à risque.",
+    courtsRulesMisconduct: "Le harcèlement, la manipulation ou les preuves fabriquées peuvent entraîner une résolution négative immédiate.",
+    courtsRulesGlossary: "Affaire : enregistrement d’un conflit. Preuves : éléments à l’appui des affirmations. Verdict : décision avec remède. Appel : demande de réexamen du verdict.",
+    courtsFilterActions: "ACTIONS",
+    courtsNoActions: "Aucune action en attente pour votre rôle.",
+    courtsCaseTitlePlaceholder: "Brève description du conflit",
+    courtsCaseSeverity: "Gravité",
+    courtsCaseSeverityNone: "Sans étiquette de gravité",
+    courtsCaseSeverityLOW: "Faible",
+    courtsCaseSeverityMEDIUM: "Moyenne",
+    courtsCaseSeverityHIGH: "Élevée",
+    courtsCaseSeverityCRITICAL: "Critique",
+    courtsCaseSubject: "Sujet",
+    courtsCaseSubjectNone: "Sans étiquette de sujet",
+    courtsCaseSubjectBEHAVIOUR: "Comportement",
+    courtsCaseSubjectCONTENT: "Contenu",
+    courtsCaseSubjectGOVERNANCE: "Gouvernance / règles",
+    courtsCaseSubjectFINANCIAL: "Financier / ressources",
+    courtsCaseSubjectOTHER: "Autre",
+    courtsHiddenRespondent: "Caché (visible uniquement pour les rôles impliqués).",
+    courtsThRole: "Rôle",
+    courtsRoleAccuser: "Accusation",
+    courtsRoleDefence: "Défense",
+    courtsRoleMediator: "Médiateur",
+    courtsRoleJudge: "Juge",
+    courtsRoleDictator: "Dictateur",
+    courtsAssignJudgeTitle: "Choisir un juge",
+    courtsAssignJudgeBtn: "Choisir un juge",
     //trending
     //trending
     trendingTitle: "Tendances",
     trendingTitle: "Tendances",
     exploreTrending: "Explorez le contenu le plus populaire dans votre réseau.",
     exploreTrending: "Explorez le contenu le plus populaire dans votre réseau.",
@@ -1177,9 +1292,9 @@ module.exports = {
     yourActivity:         "Votre activité",
     yourActivity:         "Votre activité",
     globalActivity:       "Activité globale",
     globalActivity:       "Activité globale",
     activityList:         "Activité",
     activityList:         "Activité",
-    activityDesc:         "Consultez l’activité récente de votre réseau.",
-    allButton:            "TOUS",
-    mineButton:           "MIEN",
+    activityDesc:         "Découvrez l’activité récente de votre réseau.",
+    allButton:            "TOUT",
+    mineButton:           "MIENNES",
     noActions:            "Aucune activité disponible.",
     noActions:            "Aucune activité disponible.",
     performed:            "→",
     performed:            "→",
     from:                 "De",
     from:                 "De",
@@ -1197,40 +1312,60 @@ module.exports = {
     typeRecent:           "RÉCENT",
     typeRecent:           "RÉCENT",
     errorActivity:        "Erreur lors de la récupération de l’activité",
     errorActivity:        "Erreur lors de la récupération de l’activité",
     typePost:             "PUBLICATION",
     typePost:             "PUBLICATION",
-    typeTribe:            "TRIBU",
-    typeAbout:            "HABITANT",
+    typeTribe:            "TRIBUS",
+    typeAbout:            "HABITANTS",
     typeCurriculum:       "CV",
     typeCurriculum:       "CV",
-    typeImage:            "IMAGE",
-    typeBookmark:         "MARQUE-PAGE",
-    typeDocument:         "DOCUMENT",
-    typeVotes:            "GOUVERNANCE",
-    typeAudio:            "AUDIO",
+    typeImage:            "IMAGES",
+    typeBookmark:         "SIGNETS",
+    typeDocument:         "DOCUMENTS",
+    typeVotes:            "VOTES",
+    typeAudio:            "AUDIOS",
     typeMarket:           "MARCHÉ",
     typeMarket:           "MARCHÉ",
-    typeJob:              "EMPLOI",
-    typeProject:          "PROJET",
-    typeVideo:            "VIDÉO",
-    typeVote:             "DIFFUSION",
-    typeEvent:            "ÉVÉNEMENT",
+    typeJob:              "EMPLOIS",
+    typeProject:          "PROJETS",
+    typeVideo:            "VIDÉOS",
+    typeVote:             "PARTAGE",
+    typeEvent:            "ÉVÉNEMENTS",
     typeTransfer:         "TRANSFERT",
     typeTransfer:         "TRANSFERT",
     typeTask:             "TÂCHES",
     typeTask:             "TÂCHES",
     typePixelia:          "PIXELIA",
     typePixelia:          "PIXELIA",
     typeForum:            "FORUM",
     typeForum:            "FORUM",
-    typeReport:           "RAPPORT",
-    typeFeed:             "FEED",
+    typeReport:           "RAPPORTS",
+    typeFeed:             "FLUX",
     typeContact:          "CONTACT",
     typeContact:          "CONTACT",
     typePub:              "PUB",
     typePub:              "PUB",
     typeTombstone:        "TOMBSTONE",
     typeTombstone:        "TOMBSTONE",
     typeBanking:          "BANQUE",
     typeBanking:          "BANQUE",
+    typeBankWallet:       "BANQUE/PORTEFEUILLE",
+    typeBankClaim:        "BANQUE/UBI",
+    typeKarmaScore:       "KARMA",
+    typeParliament:       "PARLEMENT",
+    typeParliamentCandidature: "Parlement · Candidature",
+    typeParliamentTerm:   "Parlement · Mandat",
+    typeParliamentProposal:"Parlement · Proposition",
+    typeParliamentRevocation:"Parlement · Révocation",
+    typeParliamentLaw:    "Parlement · Nouvelle loi",
+    typeCourts:           "TRIBUNAUX",
+    typeCourtsCase:       "Tribunaux · Affaire",
+    typeCourtsEvidence:   "Tribunaux · Preuve",
+    typeCourtsAnswer:     "Tribunaux · Réponse",
+    typeCourtsVerdict:    "Tribunaux · Verdict",
+    typeCourtsSettlement: "Tribunaux · Règlement",
+    typeCourtsSettlementProposal: "Tribunaux · Proposition de règlement",
+    typeCourtsSettlementAccepted: "Tribunaux · Règlement accepté",
+    typeCourtsNomination: "Tribunaux · Nomination",
+    typeCourtsNominationVote: "Tribunaux · Vote de nomination",
     activitySupport:      "Nouvelle alliance forgée",
     activitySupport:      "Nouvelle alliance forgée",
     activityJoin:         "Nouveau PUB rejoint",
     activityJoin:         "Nouveau PUB rejoint",
     question:             "Question",
     question:             "Question",
-    deadline:             "Date limite",
-    status:               "État",
+    deadline:             "Échéance",
+    status:               "Statut",
     votes:                "Votes",
     votes:                "Votes",
-    totalVotes:           "Total des votes",
+    totalVotes:           "Votes totaux",
+    voteTotalVotes:       "Votes totaux",
     name:                 "Nom",
     name:                 "Nom",
     skills:               "Compétences",
     skills:               "Compétences",
-    tags:                 "Étiquettes",
+    tags:                 "Tags",
     title:                "Titre",
     title:                "Titre",
     date:                 "Date",
     date:                 "Date",
     category:             "Catégorie",
     category:             "Catégorie",
@@ -1238,22 +1373,94 @@ module.exports = {
     activitySpread:       "->",
     activitySpread:       "->",
     visitLink:            "Visiter le lien",
     visitLink:            "Visiter le lien",
     viewDocument:         "Voir le document",
     viewDocument:         "Voir le document",
-    location:             "Localisation",
+    location:             "Lieu",
     contentWarning:       "Sujet",
     contentWarning:       "Sujet",
     personName:           "Nom de l’habitant",
     personName:           "Nom de l’habitant",
-    typeBankWallet:       "BANQUE/PORTFEUILLE",
-    typeBankClaim:        "BANQUE/UBI",
-    typeKarmaScore:	  "KARMA",
     bankWalletConnected:  "Portefeuille ECOin",
     bankWalletConnected:  "Portefeuille ECOin",
-    bankUbiReceived:      "UBI reçue",
+    bankUbiReceived:      "UBI reçu",
     bankTx:               "Tx",
     bankTx:               "Tx",
     bankEpochShort:       "Époque",
     bankEpochShort:       "Époque",
-    activityProjectFollow: "%OASIS% est maintenant en train de %ACTION% ce projet %PROJECT%",
-    activityProjectUnfollow: "%OASIS% est maintenant en train de %ACTION% ce projet %PROJECT%",
-    activityProjectPledged: "%OASIS% a %ACTION% %AMOUNT% au projet %PROJECT%",
-    following: "SUIVRE",
-    unfollowing: "CESSER DE SUIVRE",
-    pledged: "CONTRIBUÉ",
+    bankAllocId:          "Identifiant d’allocation",
+    bankingUserEngagementScore: "Score d’engagement utilisateur",
+    viewDetails:          "Voir les détails",
+    link:                 "Lien",
+    aiSnippetsLearned:    "Extraits appris",
+    tribeFeedRefeeds:     "Repartages",
+    activityProjectFollow:   "%OASIS% est maintenant %ACTION% ce projet %PROJECT%",
+    activityProjectUnfollow: "%OASIS% est maintenant %ACTION% ce projet %PROJECT%",
+    activityProjectPledged:  "%OASIS% a %ACTION% %AMOUNT% au projet %PROJECT%",
+    following:            "SUIVRE",
+    unfollowing:          "NE PLUS SUIVRE",
+    pledged:              "ENGAGÉ",
+    parliamentCandidatureId: "Candidature",
+    parliamentGovMethod:     "Méthode",
+    parliamentVotesReceived: "Votes reçus",
+    parliamentMethodANARCHY:   "Anarchie",
+    parliamentMethodVOTE:      "Vote communautaire",
+    parliamentMethodRANKED:    "Vote préférentiel",
+    parliamentMethodPLURALITY: "Pluralité",
+    parliamentMethodCOUNCIL:   "Conseil",
+    parliamentMethodJURY:      "Jury",
+    parliamentAnarchy:         "ANARCHIE",
+    parliamentElectionsStart:  "Début des élections",
+    parliamentElectionsEnd:    "Fin des élections",
+    parliamentCurrentLeader:   "Candidature gagnante",
+    parliamentProposalTitle:   "Titre",
+    parliamentOpenVote:        "Vote ouvert",
+    parliamentStatus:          "Statut",
+    parliamentLawQuestion:     "Question",
+    parliamentLawMethod:       "Méthode",
+    parliamentLawProposer:     "Proposant",
+    parliamentLawEnacted:      "Promulgué le",
+    parliamentLawVotes:        "Votes",
+    createdAt:                 "Créé le",
+    courtsCaseTitle:           "Affaire",
+    courtsMethod:              "Méthode",
+    courtsMethodJUDGE:         "Juge",
+    courtsMethodJUDGES:        "Collège de juges",
+    courtsMethodSINGLE_JUDGE:  "Juge unique",
+    courtsMethodJURY:          "Jury",
+    courtsMethodCOUNCIL:       "Conseil",
+    courtsMethodCOMMUNITY:     "Communauté",
+    courtsMethodMEDIATION:     "Médiation",
+    courtsMethodARBITRATION:   "Arbitrage",
+    courtsMethodVOTE:          "Vote communautaire",
+    courtsAccuser:             "Accusation",
+    courtsRespondent:          "Défense",
+    courtsThStatus:            "Statut",
+    courtsThAnswerBy:          "Réponse avant",
+    courtsThEvidenceBy:        "Preuves avant",
+    courtsThDecisionBy:        "Décision avant",
+    courtsVotesNeeded:         "Votes nécessaires",
+    courtsVotesSlashTotal:     "OUI/TOTAL",
+    courtsOpenVote:            "Vote ouvert",
+    courtsAnswerTitle:         "Réponse",
+    courtsStanceADMIT:         "Admettre",
+    courtsStanceDENY:          "Refuser",
+    courtsStancePARTIAL:       "Partiel",
+    courtsStanceCOUNTERCLAIM:  "Demande reconventionnelle",
+    courtsStanceNEUTRAL:       "Neutre",
+    courtsVerdictResult:       "Résultat",
+    courtsVerdictOrders:       "Ordonnances",
+    courtsSettlementText:      "Accord",
+    courtsSettlementAccepted:  "Accepté",
+    courtsSettlementPending:   "En attente",
+    courtsJudge:               "Juge",
+    courtsThSupports:          "Soutiens",
+    courtsFilterOpenCase: 'Ouvrir un dossier',
+    courtsEvidenceFileLabel: 'Fichier de preuve (image, audio, vidéo ou PDF)',
+    courtsCaseMediators: 'Médiateurs',
+    courtsCaseMediatorsPh: 'IDs Oasis des médiateurs, séparés par des virgules',
+    courtsMediatorsLabel: 'Médiateurs',
+    courtsThCase: 'Dossier',
+    courtsThCreatedAt: 'Date de début',
+    courtsThActions: 'Actions',
+    courtsPublicPrefLabel: 'Visibilité après la résolution',
+    courtsPublicPrefYes: "J'accepte que ce dossier soit entièrement public",
+    courtsPublicPrefNo: 'Je préfère garder les détails privés',
+    courtsPublicPrefSubmit: 'Enregistrer la préférence de visibilité',
+    courtsMethodMEDIATION: 'Médiation',
+    courtsNoCases: 'Aucun dossier.',
     //reports
     //reports
     reportsTitle: "Rapports",
     reportsTitle: "Rapports",
     reportsDescription: "Gérez et suivez les rapports liés aux problèmes, aux erreurs, aux abus et aux avertissements de contenu dans votre réseau.",
     reportsDescription: "Gérez et suivez les rapports liés aux problèmes, aux erreurs, aux abus et aux avertissements de contenu dans votre réseau.",
@@ -1602,61 +1809,48 @@ module.exports = {
     //stats
     //stats
     statsTitle: 'Statistiques',
     statsTitle: 'Statistiques',
     statistics: "Statistiques",
     statistics: "Statistiques",
-    statsInhabitant: "Statistiques des habitants",
-    statsDescription: "Découvrez les statistiques de votre réseau.",
-    ALLButton: "TOUS",
-    MINEButton: "MES",
-    TOMBSTONEButton: "PIERRES TOMB.",
+    statsInhabitant: "Statistiques de l'habitant",
+    statsDescription: "Découvrez des statistiques sur votre réseau.",
+    ALLButton: "TOUT",
+    MINEButton: "MIENS",
+    TOMBSTONEButton: "SUPPRESSIONS",
     statsYou: "Vous",
     statsYou: "Vous",
-    statsUserId: "ID d’Oasis",
+    statsUserId: "ID Oasis",
     statsCreatedAt: "Créé le",
     statsCreatedAt: "Créé le",
     statsYourContent: "Contenu",
     statsYourContent: "Contenu",
     statsYourOpinions: "Avis",
     statsYourOpinions: "Avis",
-    statsYourTombstone: "Pierres tombales",
-    statsYourProject: "Projets",
-    statsDiscoveredProject: "Projets",
-    statsBankingTitle: "Banque",
-    statsEcoWalletLabel: "Portefeuille ECOIN",
-    statsEcoWalletNotConfigured: "Non configuré !",
-    statsTotalEcoAddresses: "Total des adresses",
-    statsProject: "Projets",
+    statsYourTombstone: "Suppressions",
     statsNetwork: "Réseau",
     statsNetwork: "Réseau",
     statsTotalInhabitants: "Habitants",
     statsTotalInhabitants: "Habitants",
-    statsDiscoveredTribes: "Tribus (Publiques)",
-    statsPrivateDiscoveredTribes: "Tribus (Privées)",
+    statsDiscoveredTribes: "Tribus (publiques)",
+    statsPrivateDiscoveredTribes: "Tribus (privées)",
     statsNetworkContent: "Contenu",
     statsNetworkContent: "Contenu",
     statsYourMarket: "Marché",
     statsYourMarket: "Marché",
     statsYourJob: "Emplois",
     statsYourJob: "Emplois",
+    statsYourProject: "Projets",
     statsYourTransfer: "Transferts",
     statsYourTransfer: "Transferts",
     statsYourForum: "Forums",   
     statsYourForum: "Forums",   
-    statsProject: "Projets",
-    statsProjectsTitle: "Projets",
-    statsProjectsTotal: "Total des projets",
-    statsProjectsActive: "Actifs",
-    statsProjectsCompleted: "Terminés",
-    statsProjectsPaused: "En pause",
-    statsProjectsCancelled: "Annulés",
-    statsProjectsGoalTotal: "Objectif total",
-    statsProjectsPledgedTotal: "Total engagé",
-    statsProjectsSuccessRate: "Taux de réussite",
-    statsProjectsAvgProgress: "Progression moyenne",
-    statsProjectsMedianProgress: "Progression médiane",
-    statsProjectsActiveFundingAvg: "Financement actif moyen",
     statsNetworkOpinions: "Avis",
     statsNetworkOpinions: "Avis",
     statsDiscoveredMarket: "Marché",
     statsDiscoveredMarket: "Marché",
     statsDiscoveredJob: "Emplois",
     statsDiscoveredJob: "Emplois",
+    statsDiscoveredProject: "Projets",
+    statsBankingTitle: "Banque",
+    statsEcoWalletLabel: "Portefeuille ECOIN",
+    statsEcoWalletNotConfigured:  "Non configuré !",
+    statsTotalEcoAddresses: "Adresses totales",
     statsDiscoveredTransfer: "Transferts",
     statsDiscoveredTransfer: "Transferts",
     statsDiscoveredForum: "Forums",
     statsDiscoveredForum: "Forums",
-    statsNetworkTombstone: "Pierres tombales",
-    statsBookmark: "Marque-pages",
+    statsNetworkTombstone: "Suppressions",
+    statsBookmark: "Signets",
     statsEvent: "Événements",
     statsEvent: "Événements",
     statsTask: "Tâches",
     statsTask: "Tâches",
     statsVotes: "Votes",
     statsVotes: "Votes",
     statsMarket: "Marché",
     statsMarket: "Marché",
     statsForum: "Forums",
     statsForum: "Forums",
     statsJob: "Emplois",
     statsJob: "Emplois",
+    statsProject: "Projets",
     statsReport: "Rapports",
     statsReport: "Rapports",
-    statsFeed: "Feeds",
+    statsFeed: "Flux",
     statsTribe: "Tribus",
     statsTribe: "Tribus",
     statsImage: "Images",
     statsImage: "Images",
     statsAudio: "Audios",
     statsAudio: "Audios",
@@ -1666,52 +1860,69 @@ module.exports = {
     statsAiExchange: "IA",
     statsAiExchange: "IA",
     statsPUBs: 'PUBs',
     statsPUBs: 'PUBs',
     statsPost: "Publications",
     statsPost: "Publications",
-    statsOasisID: "ID d’Oasis",
+    statsOasisID: "ID Oasis",
     statsSize: "Total (taille)",
     statsSize: "Total (taille)",
     statsBlockchainSize: "Blockchain (taille)",
     statsBlockchainSize: "Blockchain (taille)",
-    statsBlobsSize: "Blobs (taille)",   
+    statsBlobsSize: "Blobs (taille)",
     statsActivity7d: "Activité (7 derniers jours)",
     statsActivity7d: "Activité (7 derniers jours)",
     statsActivity7dTotal: "Total 7 jours",
     statsActivity7dTotal: "Total 7 jours",
     statsActivity30dTotal: "Total 30 jours",
     statsActivity30dTotal: "Total 30 jours",
-    statsKarmaScore: "Score de KARMA",
+    statsKarmaScore: "Score KARMA",
     statsPublic: "Public",
     statsPublic: "Public",
     statsPrivate: "Privé",
     statsPrivate: "Privé",
     day: "Jour",
     day: "Jour",
     messages: "Messages",
     messages: "Messages",
     statsProject: "Projets",
     statsProject: "Projets",
     statsProjectsTitle: "Projets",
     statsProjectsTitle: "Projets",
-    statsProjectsTotal: "Total des projets",
+    statsProjectsTotal: "Projets au total",
     statsProjectsActive: "Actifs",
     statsProjectsActive: "Actifs",
     statsProjectsCompleted: "Terminés",
     statsProjectsCompleted: "Terminés",
     statsProjectsPaused: "En pause",
     statsProjectsPaused: "En pause",
     statsProjectsCancelled: "Annulés",
     statsProjectsCancelled: "Annulés",
     statsProjectsGoalTotal: "Objectif total",
     statsProjectsGoalTotal: "Objectif total",
-    statsProjectsPledgedTotal: "Total engagé",
+    statsProjectsPledgedTotal: "Montant promis total",
     statsProjectsSuccessRate: "Taux de réussite",
     statsProjectsSuccessRate: "Taux de réussite",
     statsProjectsAvgProgress: "Progression moyenne",
     statsProjectsAvgProgress: "Progression moyenne",
     statsProjectsMedianProgress: "Progression médiane",
     statsProjectsMedianProgress: "Progression médiane",
     statsProjectsActiveFundingAvg: "Financement actif moyen",
     statsProjectsActiveFundingAvg: "Financement actif moyen",
     statsJobsTitle: "Emplois",
     statsJobsTitle: "Emplois",
-    statsJobsTotal: "Total des emplois",
+    statsJobsTotal: "Emplois au total",
     statsJobsOpen: "Ouverts",
     statsJobsOpen: "Ouverts",
     statsJobsClosed: "Fermés",
     statsJobsClosed: "Fermés",
     statsJobsOpenVacants: "Postes ouverts",
     statsJobsOpenVacants: "Postes ouverts",
-    statsJobsSubscribersTotal: "Total des abonnés",
+    statsJobsSubscribersTotal: "Abonnés au total",
     statsJobsAvgSalary: "Salaire moyen",
     statsJobsAvgSalary: "Salaire moyen",
     statsJobsMedianSalary: "Salaire médian",
     statsJobsMedianSalary: "Salaire médian",
     statsMarketTitle: "Marché",
     statsMarketTitle: "Marché",
-    statsMarketTotal: "Total des articles",
-    statsMarketForSale: "En vente",
+    statsMarketTotal: "Articles au total",
+    statsMarketForSale: "À vendre",
     statsMarketReserved: "Réservés",
     statsMarketReserved: "Réservés",
     statsMarketClosed: "Fermés",
     statsMarketClosed: "Fermés",
     statsMarketSold: "Vendus",
     statsMarketSold: "Vendus",
-    statsMarketRevenue: "Revenus",
+    statsMarketRevenue: "Chiffre d'affaires",
     statsMarketAvgSoldPrice: "Prix de vente moyen",
     statsMarketAvgSoldPrice: "Prix de vente moyen",
     statsUsersTitle: "Habitants",
     statsUsersTitle: "Habitants",
     user: "Habitant",
     user: "Habitant",
-    statsTombstoneTitle: "Tombes",
-    statsNetworkTombstones: "Tombes du réseau",
-    statsTombstoneRatio: "Taux de tombes (%)",
+    statsTombstoneTitle: "Suppressions",
+    statsNetworkTombstones: "Suppressions du réseau",
+    statsTombstoneRatio: "Taux de suppressions (%)",
+    statsAITraining: "Entraînement IA",
+    statsAIExchanges: "Échanges",
+    bankingUserEngagementScore: "Score d'engagement utilisateur",
+    statsParliamentCandidature: "Candidatures au parlement",
+    statsParliamentTerm: "Mandats parlementaires",
+    statsParliamentProposal: "Propositions parlementaires",
+    statsParliamentRevocation: "Révocations parlementaires",
+    statsParliamentLaw: "Lois parlementaires",
+    statsCourtsCase: "Affaires judiciaires",
+    statsCourtsEvidence: "Preuves judiciaires",
+    statsCourtsAnswer: "Réponses judiciaires",
+    statsCourtsVerdict: "Verdicts judiciaires",
+    statsCourtsSettlement: "Accords judiciaires",
+    statsCourtsSettlementProposal: "Propositions d'accord",
+    statsCourtsSettlementAccepted: "Accords acceptés",
+    statsCourtsNomination: "Nominations de juges",
+    statsCourtsNominationVote: "Votes de nomination",
     //AI
     //AI
     ai: "IA",
     ai: "IA",
     aiTitle: "IA",
     aiTitle: "IA",
@@ -2014,6 +2225,8 @@ module.exports = {
     modulesVotationsDescription: "Module pour découvrir et gérer les votations.",  
     modulesVotationsDescription: "Module pour découvrir et gérer les votations.",  
     modulesParliamentLabel: "Parlement",
     modulesParliamentLabel: "Parlement",
     modulesParliamentDescription: "Module pour élire des gouvernements et voter des lois.",
     modulesParliamentDescription: "Module pour élire des gouvernements et voter des lois.",
+    modulesCourtsLabel: "Tribunaux",
+    modulesCourtsDescription: "Module pour résoudre des conflits et émettre des verdicts.",
     modulesReportsLabel: "Rapports",
     modulesReportsLabel: "Rapports",
     modulesReportsDescription: "Module pour gérer et suivre les rapports liés aux problèmes, erreurs, abus et avertissements de contenu.",
     modulesReportsDescription: "Module pour gérer et suivre les rapports liés aux problèmes, erreurs, abus et avertissements de contenu.",
     modulesOpinionsLabel: "Opinions",
     modulesOpinionsLabel: "Opinions",

+ 2 - 1
src/configs/config-manager.js

@@ -42,7 +42,8 @@ if (!fs.existsSync(configFilePath)) {
       "jobsMod": "on",
       "jobsMod": "on",
       "projectsMod": "on",
       "projectsMod": "on",
       "bankingMod": "on",
       "bankingMod": "on",
-      "parliamentMod": "on"
+      "parliamentMod": "on",
+      "courtsMod": "on"
     },
     },
     "wallet": {
     "wallet": {
       "url": "http://localhost:7474",
       "url": "http://localhost:7474",

+ 2 - 1
src/configs/oasis-config.json

@@ -36,7 +36,8 @@
     "jobsMod": "on",
     "jobsMod": "on",
     "projectsMod": "on",
     "projectsMod": "on",
     "bankingMod": "on",
     "bankingMod": "on",
-    "parliamentMod": "on"
+    "parliamentMod": "on",
+    "courtsMod": "on"
   },
   },
   "wallet": {
   "wallet": {
     "url": "http://localhost:7474",
     "url": "http://localhost:7474",

+ 17 - 12
src/models/activity_model.js

@@ -12,6 +12,15 @@ function inferType(c = {}) {
   if (c.type === 'wallet' && c.coin === 'ECO' && typeof c.address === 'string') return 'bankWallet';
   if (c.type === 'wallet' && c.coin === 'ECO' && typeof c.address === 'string') return 'bankWallet';
   if (c.type === 'bankClaim') return 'bankClaim';
   if (c.type === 'bankClaim') return 'bankClaim';
   if (c.type === 'karmaScore') return 'karmaScore';
   if (c.type === 'karmaScore') return 'karmaScore';
+  if (c.type === 'courts_case') return 'courtsCase';
+  if (c.type === 'courts_evidence') return 'courtsEvidence';
+  if (c.type === 'courts_answer') return 'courtsAnswer';
+  if (c.type === 'courts_verdict') return 'courtsVerdict';
+  if (c.type === 'courts_settlement') return 'courtsSettlement';
+  if (c.type === 'courts_nomination') return 'courtsNomination';
+  if (c.type === 'courts_nom_vote') return 'courtsNominationVote';
+  if (c.type === 'courts_public_pref') return 'courtsPublicPref';
+  if (c.type === 'courts_mediators') return 'courtsMediators';
   return c.type || '';
   return c.type || '';
 }
 }
 
 
@@ -91,14 +100,11 @@ module.exports = ({ cooler }) => {
 
 
           if (ev.content.followersOp === 'follow') kind = 'follow';
           if (ev.content.followersOp === 'follow') kind = 'follow';
           else if (ev.content.followersOp === 'unfollow') kind = 'unfollow';
           else if (ev.content.followersOp === 'unfollow') kind = 'unfollow';
-
           if (ev.content.backerPledge && typeof ev.content.backerPledge.amount !== 'undefined') {
           if (ev.content.backerPledge && typeof ev.content.backerPledge.amount !== 'undefined') {
             const amt = Math.max(0, parseFloat(ev.content.backerPledge.amount || 0) || 0);
             const amt = Math.max(0, parseFloat(ev.content.backerPledge.amount || 0) || 0);
             if (amt > 0) { kind = kind || 'pledge'; amount = amt }
             if (amt > 0) { kind = kind || 'pledge'; amount = amt }
           }
           }
-
           if (!kind) continue;
           if (!kind) continue;
-
           const augmented = {
           const augmented = {
             ...ev,
             ...ev,
             type: 'project',
             type: 'project',
@@ -114,7 +120,6 @@ module.exports = ({ cooler }) => {
           idToTipId.set(ev.id, ev.id);
           idToTipId.set(ev.id, ev.id);
         }
         }
       }
       }
-
       const latest = [];
       const latest = [];
       for (const a of idToAction.values()) {
       for (const a of idToAction.values()) {
         if (tombstoned.has(a.id)) continue;
         if (tombstoned.has(a.id)) continue;
@@ -137,21 +142,17 @@ module.exports = ({ cooler }) => {
         }
         }
         latest.push({ ...a, tipId: idToTipId.get(a.id) || a.id });
         latest.push({ ...a, tipId: idToTipId.get(a.id) || a.id });
       }
       }
-
       let deduped = latest.filter(a => !a.tipId || a.tipId === a.id);
       let deduped = latest.filter(a => !a.tipId || a.tipId === a.id);
-
       const mediaTypes = new Set(['image','video','audio','document','bookmark']);
       const mediaTypes = new Set(['image','video','audio','document','bookmark']);
       const perAuthorUnique = new Set(['karmaScore']);
       const perAuthorUnique = new Set(['karmaScore']);
       const byKey = new Map();
       const byKey = new Map();
       const norm = s => String(s || '').trim().toLowerCase();
       const norm = s => String(s || '').trim().toLowerCase();
-
       for (const a of deduped) {
       for (const a of deduped) {
         const c = a.content || {};
         const c = a.content || {};
         const effTs =
         const effTs =
           (c.updatedAt && Date.parse(c.updatedAt)) ||
           (c.updatedAt && Date.parse(c.updatedAt)) ||
           (c.createdAt && Date.parse(c.createdAt)) ||
           (c.createdAt && Date.parse(c.createdAt)) ||
           (a.ts || 0);
           (a.ts || 0);
-
         if (mediaTypes.has(a.type)) {
         if (mediaTypes.has(a.type)) {
           const u = c.url || c.title || `${a.type}:${a.id}`;
           const u = c.url || c.title || `${a.type}:${a.id}`;
           const key = `${a.type}:${u}`;
           const key = `${a.type}:${u}`;
@@ -177,7 +178,6 @@ module.exports = ({ cooler }) => {
         }
         }
       }
       }
       deduped = Array.from(byKey.values()).map(x => { delete x.__effTs; return x });
       deduped = Array.from(byKey.values()).map(x => { delete x.__effTs; return x });
-
       let out;
       let out;
       if (filter === 'mine') out = deduped.filter(a => a.author === userId);
       if (filter === 'mine') out = deduped.filter(a => a.author === userId);
       else if (filter === 'recent') { const cutoff = Date.now() - 24 * 60 * 60 * 1000; out = deduped.filter(a => (a.ts || 0) >= cutoff) }
       else if (filter === 'recent') { const cutoff = Date.now() - 24 * 60 * 60 * 1000; out = deduped.filter(a => (a.ts || 0) >= cutoff) }
@@ -185,9 +185,14 @@ module.exports = ({ cooler }) => {
       else if (filter === 'banking') out = deduped.filter(a => a.type === 'bankWallet' || a.type === 'bankClaim');
       else if (filter === 'banking') out = deduped.filter(a => a.type === 'bankWallet' || a.type === 'bankClaim');
       else if (filter === 'karma') out = deduped.filter(a => a.type === 'karmaScore');
       else if (filter === 'karma') out = deduped.filter(a => a.type === 'karmaScore');
       else if (filter === 'parliament')
       else if (filter === 'parliament')
-      out = deduped.filter(a =>
-        ['parliamentCandidature','parliamentTerm','parliamentProposal','parliamentRevocation','parliamentLaw'].includes(a.type)
-      );
+        out = deduped.filter(a =>
+          ['parliamentCandidature','parliamentTerm','parliamentProposal','parliamentRevocation','parliamentLaw'].includes(a.type)
+        );
+      else if (filter === 'courts')
+        out = deduped.filter(a => {
+          const t = String(a.type || '').toLowerCase();
+          return t === 'courtscase' || t === 'courtsnomination' || t === 'courtsnominationvote';
+        });
       else out = deduped.filter(a => a.type === filter);
       else out = deduped.filter(a => a.type === filter);
 
 
       out.sort((a, b) => (b.ts || 0) - (a.ts || 0));
       out.sort((a, b) => (b.ts || 0) - (a.ts || 0));

+ 19 - 0
src/models/banking_model.js

@@ -475,11 +475,13 @@ async function fetchUserActions(userId) {
   });
   });
 }
 }
 
 
+// karma scoring table
 function scoreFromActions(actions) {
 function scoreFromActions(actions) {
   let score = 0;
   let score = 0;
   for (const action of actions) {
   for (const action of actions) {
     const t = normalizeType(action);
     const t = normalizeType(action);
     const c = action.content || {};
     const c = action.content || {};
+    const rawType = String(c.type || "").toLowerCase();
     if (t === "post") score += 10;
     if (t === "post") score += 10;
     else if (t === "comment") score += 5;
     else if (t === "comment") score += 5;
     else if (t === "like") score += 2;
     else if (t === "like") score += 2;
@@ -507,6 +509,23 @@ function scoreFromActions(actions) {
     else if (t === "about") score += 1;
     else if (t === "about") score += 1;
     else if (t === "contact") score += 1;
     else if (t === "contact") score += 1;
     else if (t === "pub") score += 1;
     else if (t === "pub") score += 1;
+    else if (t === "parliamentcandidature" || rawType === "parliamentcandidature") score += 12;
+    else if (t === "parliamentterm" || rawType === "parliamentterm") score += 25;
+    else if (t === "parliamentproposal" || rawType === "parliamentproposal") score += 8;
+    else if (t === "parliamentlaw" || rawType === "parliamentlaw") score += 16;
+    else if (t === "parliamentrevocation" || rawType === "parliamentrevocation") score += 10;
+    else if (t === "courts_case" || t === "courtscase" || rawType === "courts_case") score += 4;
+    else if (t === "courts_evidence" || t === "courtsevidence" || rawType === "courts_evidence") score += 3;
+    else if (t === "courts_answer" || t === "courtsanswer" || rawType === "courts_answer") score += 4;
+    else if (t === "courts_verdict" || t === "courtsverdict" || rawType === "courts_verdict") score += 10;
+    else if (t === "courts_settlement" || t === "courtssettlement" || rawType === "courts_settlement") score += 8;
+    else if (t === "courts_nomination" || t === "courtsnomination" || rawType === "courts_nomination") score += 6;
+    else if (t === "courts_nom_vote" || t === "courtsnomvote" || rawType === "courts_nom_vote") score += 3;
+    else if (t === "courts_public_pref" || t === "courtspublicpref" || rawType === "courts_public_pref") score += 1;
+    else if (t === "courts_mediators" || t === "courtsmediators" || rawType === "courts_mediators") score += 6;
+    else if (t === "courts_open_support" || t === "courtsopensupport" || rawType === "courts_open_support") score += 2;
+    else if (t === "courts_verdict_vote" || t === "courtsverdictvote" || rawType === "courts_verdict_vote") score += 3;
+    else if (t === "courts_judge_assign" || t === "courtsjudgeassign" || rawType === "courts_judge_assign") score += 5;
   }
   }
   return Math.max(0, Math.round(score));
   return Math.max(0, Math.round(score));
 }
 }

+ 4 - 0
src/models/blockchain_model.js

@@ -115,6 +115,10 @@ module.exports = ({ cooler }) => {
         const pset = new Set(['parliamentTerm','parliamentProposal','parliamentLaw','parliamentCandidature','parliamentRevocation']);
         const pset = new Set(['parliamentTerm','parliamentProposal','parliamentLaw','parliamentCandidature','parliamentRevocation']);
         filtered = blockData.filter(b => b && pset.has(b.type));
         filtered = blockData.filter(b => b && pset.has(b.type));
       }
       }
+      if (filter === 'COURTS' || filter === 'courts') {
+        const cset = new Set(['courtsCase','courtsEvidence','courtsAnswer','courtsVerdict','courtsSettlement','courtsSettlementProposal','courtsSettlementAccepted','courtsNomination','courtsNominationVote']);
+        filtered = blockData.filter(b => b && cset.has(b.type));
+      }
 
 
       return filtered.filter(Boolean);
       return filtered.filter(Boolean);
     },
     },

+ 625 - 0
src/models/courts_model.js

@@ -0,0 +1,625 @@
+const pull = require('../server/node_modules/pull-stream');
+const moment = require('../server/node_modules/moment');
+const { getConfig } = require('../configs/config-manager.js');
+
+const logLimit = getConfig().ssbLogStream?.limit || 1000;
+const CASE_ANSWER_DAYS = 7;
+const CASE_EVIDENCE_DAYS = 14;
+const CASE_DECISION_DAYS = 21;
+const POPULAR_DAYS = 14;
+const FEED_ID_RE = /^@.+\.ed25519$/;
+
+module.exports = ({ cooler, services = {} }) => {
+  let ssb;
+  let userId;
+
+  const openSsb = async () => {
+    if (!ssb) {
+      ssb = await cooler.open();
+      userId = ssb.id;
+    }
+    return ssb;
+  };
+
+  const nowISO = () => new Date().toISOString();
+  const ensureArray = (x) => (Array.isArray(x) ? x : x ? [x] : []);
+
+  async function readLog() {
+    const ssbClient = await openSsb();
+    return new Promise((resolve, reject) => {
+      pull(
+        ssbClient.createLogStream({ limit: logLimit }),
+        pull.collect((err, arr) => (err ? reject(err) : resolve(arr)))
+      );
+    });
+  }
+
+  async function listByType(type) {
+    const msgs = await readLog();
+    const tomb = new Set();
+    const rep = new Map();
+    const map = new Map();
+    for (const m of msgs) {
+      const k = m.key || m.id;
+      const c = m.value?.content || m.content;
+      if (!c) continue;
+      if (c.type === 'tombstone' && c.target) tomb.add(c.target);
+      if (c.type === type) {
+        if (c.replaces) rep.set(c.replaces, k);
+        map.set(k, { id: k, ...c });
+      }
+    }
+    for (const oldId of rep.keys()) map.delete(oldId);
+    for (const tId of tomb) map.delete(tId);
+    return [...map.values()];
+  }
+
+  async function getCurrentUserId() {
+    await openSsb();
+    return userId;
+  }
+
+  async function resolveRespondent(candidateInput) {
+    const s = String(candidateInput || '').trim();
+    if (!s) return null;
+    if (FEED_ID_RE.test(s)) {
+      return { type: 'inhabitant', id: s };
+    }
+    if (services.tribes && services.tribes.getTribeById) {
+      try {
+        const t = await services.tribes.getTribeById(s);
+        if (t && t.id) return { type: 'tribe', id: t.id };
+      } catch {}
+    }
+    return null;
+  }
+
+  function computeDeadlines(openedAt) {
+    const answerBy = moment(openedAt).add(CASE_ANSWER_DAYS, 'days').toISOString();
+    const evidenceBy = moment(openedAt).add(CASE_EVIDENCE_DAYS, 'days').toISOString();
+    const decisionBy = moment(openedAt).add(CASE_DECISION_DAYS, 'days').toISOString();
+    return { answerBy, evidenceBy, decisionBy };
+  }
+
+  async function openCase({ titleBase, respondentInput, method }) {
+    const ssbClient = await openSsb();
+    const rawTitle = String(titleBase || '').trim();
+    if (!rawTitle) throw new Error('Title is required.');
+    const resp = await resolveRespondent(respondentInput);
+    if (!resp) throw new Error('Accused / Respondent not found.');
+    const m = String(method || '').trim().toUpperCase();
+    const ALLOWED = new Set(['JUDGE', 'DICTATOR', 'POPULAR', 'MEDIATION', 'KARMATOCRACY']);
+    if (!ALLOWED.has(m)) throw new Error('Invalid resolution method.');
+    if (m === 'DICTATOR' && services.parliament && services.parliament.getGovernmentCard) {
+      try {
+        const gov = await services.parliament.getGovernmentCard();
+        const gm = String(gov && gov.method ? gov.method : '').toUpperCase();
+        if (gm !== 'DICTATORSHIP') throw new Error('DICTATOR method requires DICTATORSHIP government.');
+      } catch (e) {
+        throw new Error('Unable to verify government method for DICTATOR.');
+      }
+    }
+    const openedAt = nowISO();
+    const prefix = moment(openedAt).format('MM/YYYY') + '_';
+    const title = prefix + rawTitle;
+    const { answerBy, evidenceBy, decisionBy } = computeDeadlines(openedAt);
+    const content = {
+      type: 'courtsCase',
+      title,
+      accuser: userId,
+      respondentType: resp.type,
+      respondentId: resp.id,
+      method: m,
+      status: 'OPEN',
+      openedAt,
+      answerBy,
+      evidenceBy,
+      decisionBy,
+      mediatorsAccuser: [],
+      mediatorsRespondent: [],
+      createdAt: openedAt
+    };
+    return await new Promise((resolve, reject) =>
+      ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
+    );
+  }
+
+  async function listCases(filter = 'open') {
+    const all = await listByType('courtsCase');
+    const sorted = all.sort((a, b) => {
+      const ta = new Date(a.openedAt || a.createdAt || 0).getTime();
+      const tb = new Date(b.openedAt || b.createdAt || 0).getTime();
+      return tb - ta;
+    });
+    if (filter === 'open') {
+      return sorted.filter((c) => {
+        const s = String(c.status || '').toUpperCase();
+        return s !== 'DECIDED' && s !== 'CLOSED' && s !== 'SOLVED' && s !== 'UNSOLVED' && s !== 'DISCARDED';
+      });
+    }
+    if (filter === 'history') {
+      return sorted.filter((c) => {
+        const s = String(c.status || '').toUpperCase();
+        return s === 'DECIDED' || s === 'CLOSED' || s === 'SOLVED' || s === 'UNSOLVED' || s === 'DISCARDED';
+      });
+    }
+    return sorted;
+  }
+
+  async function listCasesForUser(uid) {
+    const all = await listByType('courtsCase');
+    const id = String(uid || userId || '');
+    const rows = [];
+    for (const c of all) {
+      const isAccuser = String(c.accuser || '') === id;
+      const isRespondent = String(c.respondentId || '') === id;
+      const ma = ensureArray(c.mediatorsAccuser || []);
+      const mr = ensureArray(c.mediatorsRespondent || []);
+      const isMediator = ma.includes(id) || mr.includes(id);
+      const isJudge = String(c.judgeId || '') === id;
+      const isDictator = false;
+      const mine = isAccuser || isRespondent || isMediator || isJudge || isDictator;
+      if (!mine) continue;
+      let myPublicPreference = null;
+      if (isAccuser && typeof c.publicPrefAccuser === 'boolean') {
+        myPublicPreference = c.publicPrefAccuser;
+      } else if (isRespondent && typeof c.publicPrefRespondent === 'boolean') {
+        myPublicPreference = c.publicPrefRespondent;
+      }
+      rows.push({
+        ...c,
+        respondent: c.respondentId || c.respondent,
+        isAccuser,
+        isRespondent,
+        isMediator,
+        isJudge,
+        isDictator,
+        mine,
+        myPublicPreference
+      });
+    }
+    rows.sort((a, b) => {
+      const ta = new Date(a.openedAt || a.createdAt || 0).getTime();
+      const tb = new Date(b.openedAt || b.createdAt || 0).getTime();
+      return tb - ta;
+    });
+    return rows;
+  }
+
+  async function getCaseById(caseId) {
+    const id = String(caseId || '').trim();
+    if (!id) return null;
+    const all = await listByType('courtsCase');
+    return all.find((c) => c.id === id) || null;
+  }
+
+  async function upsertCase(obj) {
+    const ssbClient = await openSsb();
+    const { id, ...rest } = obj;
+    const updated = {
+      ...rest,
+      type: 'courtsCase',
+      replaces: id,
+      updatedAt: nowISO()
+    };
+    return await new Promise((resolve, reject) =>
+      ssbClient.publish(updated, (err, msg) => (err ? reject(err) : resolve(msg)))
+    );
+  }
+
+  function getCaseRole(caseObj, uid) {
+    const id = String(uid || '');
+    if (!id) return 'OTHER';
+    if (String(caseObj.accuser || '') === id) return 'ACCUSER';
+    if (String(caseObj.respondentId || '') === id) return 'DEFENCE';
+    const ma = ensureArray(caseObj.mediatorsAccuser || []);
+    const mr = ensureArray(caseObj.mediatorsRespondent || []);
+    if (ma.includes(id) || mr.includes(id)) return 'MEDIATOR';
+    if (String(caseObj.judgeId || '') === id) return 'JUDGE';
+    return 'OTHER';
+  }
+
+  async function setMediators({ caseId, side, mediators }) {
+    const c = await getCaseById(caseId);
+    if (!c) throw new Error('Case not found.');
+    const role = side === 'accuser' ? 'ACCUSER' : side === 'respondent' ? 'DEFENCE' : null;
+    if (!role) throw new Error('Invalid side.');
+    const myRole = getCaseRole(c, userId);
+    if (role === 'ACCUSER' && myRole !== 'ACCUSER') throw new Error('Only accuser can set these mediators.');
+    if (role === 'DEFENCE' && myRole !== 'DEFENCE') throw new Error('Only defence can set these mediators.');
+    const list = Array.from(
+      new Set(
+        ensureArray(mediators || [])
+          .map((x) => String(x || '').trim())
+          .filter(Boolean)
+      )
+    );
+    const clean = list.filter((id) => id !== c.accuser && id !== c.respondentId);
+    if (side === 'accuser') c.mediatorsAccuser = clean;
+    else c.mediatorsRespondent = clean;
+    await upsertCase(c);
+    return c;
+  }
+
+  async function assignJudge({ caseId, judgeId }) {
+    const c = await getCaseById(caseId);
+    if (!c) throw new Error('Case not found.');
+    const m = String(c.method || '').toUpperCase();
+    if (m !== 'JUDGE') throw new Error('This case does not use a judge.');
+    const myRole = getCaseRole(c, userId);
+    if (myRole !== 'ACCUSER' && myRole !== 'DEFENCE') throw new Error('Only parties can assign a judge.');
+    const id = String(judgeId || '').trim();
+    if (!id) throw new Error('Judge ID is required.');
+    if (!FEED_ID_RE.test(id)) throw new Error('Invalid judge ID.');
+    if (id === String(c.accuser || '') || id === String(c.respondentId || '')) {
+      throw new Error('Judge cannot be a party of the case.');
+    }
+    c.judgeId = id;
+    await upsertCase(c);
+    return c;
+  }
+
+  async function addEvidence({ caseId, text, link, imageMarkdown }) {
+    const c = await getCaseById(caseId);
+    if (!c) throw new Error('Case not found.');
+    const role = getCaseRole(c, userId);
+    if (role === 'OTHER') throw new Error('You are not involved in this case.');
+    const t = String(text || '').trim();
+    const l = String(link || '').trim();
+    let imageUrl = null;
+    if (imageMarkdown) {
+      const match = imageMarkdown.match(/\(([^)]+)\)/);
+      imageUrl = match ? match[1] : imageMarkdown;
+    }
+    if (!t && !l && !imageUrl) throw new Error('Text, link or image is required.');
+    const ssbClient = await openSsb();
+    const content = {
+      type: 'courtsEvidence',
+      caseId: c.id,
+      author: userId,
+      role,
+      text: t,
+      link: l,
+      imageUrl,
+      createdAt: nowISO()
+    };
+    return await new Promise((resolve, reject) =>
+      ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
+    );
+  }
+
+  async function answerCase({ caseId, stance, text }) {
+    const c = await getCaseById(caseId);
+    if (!c) throw new Error('Case not found.');
+    if (String(c.respondentId || '') !== String(userId || '')) throw new Error('Only the respondent can answer.');
+    const s = String(stance || '').trim().toUpperCase();
+    const ALLOWED = new Set(['DENY', 'ADMIT', 'PARTIAL']);
+    if (!ALLOWED.has(s)) throw new Error('Invalid stance.');
+    const t = String(text || '').trim();
+    if (!t) throw new Error('Response text is required.');
+    const ssbClient = await openSsb();
+    const content = {
+      type: 'courtsAnswer',
+      caseId: c.id,
+      respondent: userId,
+      stance: s,
+      text: t,
+      createdAt: nowISO()
+    };
+    await new Promise((resolve, reject) =>
+      ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
+    );
+    c.status = 'IN_PROGRESS';
+    c.answeredAt = nowISO();
+    await upsertCase(c);
+    return c;
+  }
+
+  async function issueVerdict({ caseId, result, orders }) {
+    const c = await getCaseById(caseId);
+    if (!c) throw new Error('Case not found.');
+    const involved =
+      String(c.accuser || '') === String(userId || '') ||
+      String(c.respondentId || '') === String(userId || '') ||
+      ensureArray(c.mediatorsAccuser || []).includes(userId) ||
+      ensureArray(c.mediatorsRespondent || []).includes(userId);
+    if (involved) throw new Error('You cannot be judge and party in the same case.');
+    const r = String(result || '').trim();
+    if (!r) throw new Error('Result is required.');
+    const o = String(orders || '').trim();
+    const ssbClient = await openSsb();
+    const content = {
+      type: 'courtsVerdict',
+      caseId: c.id,
+      judgeId: userId,
+      result: r,
+      orders: o,
+      createdAt: nowISO()
+    };
+    await new Promise((resolve, reject) =>
+      ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
+    );
+    c.status = 'DECIDED';
+    c.verdictAt = nowISO();
+    c.judgeId = userId;
+    await upsertCase(c);
+    return c;
+  }
+
+  async function proposeSettlement({ caseId, terms }) {
+    const c = await getCaseById(caseId);
+    if (!c) throw new Error('Case not found.');
+    const role = getCaseRole(c, userId);
+    if (role === 'OTHER') throw new Error('You are not involved in this case.');
+    const t = String(terms || '').trim();
+    if (!t) throw new Error('Terms are required.');
+    const ssbClient = await openSsb();
+    const content = {
+      type: 'courtsSettlementProposal',
+      caseId: c.id,
+      proposer: userId,
+      terms: t,
+      createdAt: nowISO()
+    };
+    return await new Promise((resolve, reject) =>
+      ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
+    );
+  }
+
+  async function acceptSettlement({ caseId }) {
+    const c = await getCaseById(caseId);
+    if (!c) throw new Error('Case not found.');
+    const role = getCaseRole(c, userId);
+    if (role !== 'ACCUSER' && role !== 'DEFENCE') throw new Error('Only parties can accept a settlement.');
+    const ssbClient = await openSsb();
+    const content = {
+      type: 'courtsSettlementAccepted',
+      caseId: c.id,
+      by: userId,
+      createdAt: nowISO()
+    };
+    await new Promise((resolve, reject) =>
+      ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
+    );
+    c.status = 'CLOSED';
+    c.closedAt = nowISO();
+    await upsertCase(c);
+    return c;
+  }
+
+  async function setPublicPreference({ caseId, preference }) {
+    const c = await getCaseById(caseId);
+    if (!c) throw new Error('Case not found.');
+    const id = String(userId || '');
+    const pref = !!preference;
+    if (String(c.accuser || '') === id) {
+      c.publicPrefAccuser = pref;
+    } else if (String(c.respondentId || '') === id) {
+      c.publicPrefRespondent = pref;
+    } else {
+      throw new Error('Only parties can set visibility preference.');
+    }
+    await upsertCase(c);
+    return c;
+  }
+
+  async function openPopularVote({ caseId }) {
+    if (!services.votes || !services.votes.createVote) throw new Error('Votes service not available.');
+    const c = await getCaseById(caseId);
+    if (!c) throw new Error('Case not found.');
+    const m = String(c.method || '').toUpperCase();
+    if (m !== 'POPULAR' && m !== 'KARMATOCRACY') throw new Error('This case does not use public voting.');
+    if (c.voteId) throw new Error('Vote already opened.');
+    const question = c.title || `Case ${caseId}`;
+    const deadline = moment().add(POPULAR_DAYS, 'days').toISOString();
+    const voteMsg = await services.votes.createVote(
+      question,
+      deadline,
+      ['YES', 'NO', 'ABSTENTION'],
+      [`courtsCase:${caseId}`, `courtsMethod:${m}`]
+    );
+    c.voteId = voteMsg.key || voteMsg.id;
+    await upsertCase(c);
+    return c;
+  }
+
+  async function getInhabitantKarma(feedId) {
+    if (services.banking && services.banking.getUserEngagementScore) {
+      try {
+        const v = await services.banking.getUserEngagementScore(feedId);
+        return Number(v || 0) || 0;
+      } catch {
+        return 0;
+      }
+    }
+    return 0;
+  }
+
+  async function getFirstUserTimestamp(feedId) {
+    const ssbClient = await openSsb();
+    return new Promise((resolve) => {
+      pull(
+        ssbClient.createUserStream({ id: feedId, reverse: false }),
+        pull.filter((m) => m && m.value && m.value.content && m.value.content.type !== 'tombstone'),
+        pull.take(1),
+        pull.collect((err, arr) => {
+          if (err || !arr || !arr.length) return resolve(Date.now());
+          const m = arr[0];
+          const ts = (m.value && m.value.timestamp) || m.timestamp || Date.now();
+          resolve(ts < 1e12 ? ts * 1000 : ts);
+        })
+      );
+    });
+  }
+
+  async function nominateJudge({ judgeId }) {
+    const id = String(judgeId || '').trim();
+    if (!id) throw new Error('Judge ID is required.');
+    if (!FEED_ID_RE.test(id)) throw new Error('Invalid judge ID.');
+    const ssbClient = await openSsb();
+    const content = {
+      type: 'courtsNomination',
+      judgeId: id,
+      createdAt: nowISO()
+    };
+    return await new Promise((resolve, reject) =>
+      ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
+    );
+  }
+
+  async function voteNomination(nominationId) {
+    const id = String(nominationId || '').trim();
+    if (!id) throw new Error('Nomination not found.');
+    const nominations = await listByType('courtsNomination');
+    const nomination = nominations.find((n) => n.id === id);
+    if (!nomination) throw new Error('Nomination not found.');
+    if (String(nomination.judgeId || '') === String(userId || '')) {
+      throw new Error('You cannot vote for yourself.');
+    }
+    const votes = await listByType('courtsNominationVote');
+    const already = votes.find(
+      (v) =>
+        String(v.nominationId || '') === id &&
+        String(v.voter || '') === String(userId || '')
+    );
+    if (already) throw new Error('You have already voted.');
+    const ssbClient = await openSsb();
+    const content = {
+      type: 'courtsNominationVote',
+      nominationId: id,
+      voter: userId,
+      createdAt: nowISO()
+    };
+    return await new Promise((resolve, reject) =>
+      ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
+    );
+  }
+
+  async function listNominations() {
+    const nominations = await listByType('courtsNomination');
+    const votes = await listByType('courtsNominationVote');
+    const byId = new Map();
+    for (const n of nominations) {
+      byId.set(n.id, { ...n, supports: 0, karma: 0, profileSince: 0 });
+    }
+    for (const v of votes) {
+      const rec = byId.get(v.nominationId);
+      if (rec) rec.supports = (rec.supports || 0) + 1;
+    }
+    const rows = [];
+    for (const rec of byId.values()) {
+      const karma = await getInhabitantKarma(rec.judgeId);
+      const since = await getFirstUserTimestamp(rec.judgeId);
+      rows.push({ ...rec, karma, profileSince: since });
+    }
+    rows.sort((a, b) => {
+      if ((b.supports || 0) !== (a.supports || 0)) return (b.supports || 0) - (a.supports || 0);
+      if ((b.karma || 0) !== (a.karma || 0)) return (b.karma || 0) - (a.karma || 0);
+      if ((a.profileSince || 0) !== (b.profileSince || 0)) return (a.profileSince || 0) - (b.profileSince || 0);
+      const ta = new Date(a.createdAt || 0).getTime();
+      const tb = new Date(b.createdAt || 0).getTime();
+      if (ta !== tb) return ta - tb;
+      return String(a.judgeId || '').localeCompare(String(b.judgeId || ''));
+    });
+    return rows;
+  }
+
+  async function getCaseDetails({ caseId }) {
+    const id = String(caseId || '').trim();
+    if (!id) return null;
+    const base = await getCaseById(id);
+    if (!base) return null;
+    const currentUser = await getCurrentUserId();
+    const me = String(currentUser || '');
+    const accuserId = String(base.accuser || '');
+    const respondentId = String(base.respondentId || '');
+    const ma = ensureArray(base.mediatorsAccuser || []);
+    const mr = ensureArray(base.mediatorsRespondent || []);
+    const judgeId = String(base.judgeId || '');
+    const dictatorId = String(base.dictatorId || '');
+    const isAccuser = accuserId === me;
+    const isRespondent = respondentId === me;
+    const isMediator = ma.includes(me) || mr.includes(me);
+    const isJudge = judgeId === me;
+    const isDictator = dictatorId === me;
+    const mine = isAccuser || isRespondent || isMediator || isJudge || isDictator;
+    let myPublicPreference = null;
+    if (isAccuser && typeof base.publicPrefAccuser === 'boolean') {
+      myPublicPreference = base.publicPrefAccuser;
+    } else if (isRespondent && typeof base.publicPrefRespondent === 'boolean') {
+      myPublicPreference = base.publicPrefRespondent;
+    }
+    const publicDetails = base.publicPrefAccuser === true && base.publicPrefRespondent === true;
+    const evidencesAll = await listByType('courtsEvidence');
+    const answersAll = await listByType('courtsAnswer');
+    const settlementsAll = await listByType('courtsSettlementProposal');
+    const verdictsAll = await listByType('courtsVerdict');
+    const acceptedAll = await listByType('courtsSettlementAccepted');
+    const evidences = evidencesAll
+      .filter((e) => String(e.caseId || '') === id)
+      .sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
+    const answers = answersAll
+      .filter((a) => String(a.caseId || '') === id)
+      .sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
+    const settlements = settlementsAll
+      .filter((s) => String(s.caseId || '') === id)
+      .sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
+    const verdicts = verdictsAll
+      .filter((v) => String(v.caseId || '') === id)
+      .sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
+    const verdict = verdicts.length ? verdicts[verdicts.length - 1] : null;
+    const acceptedSettlements = acceptedAll
+      .filter((s) => String(s.caseId || '') === id)
+      .sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
+    const decidedAt =
+      base.verdictAt ||
+      base.closedAt ||
+      (verdict && verdict.createdAt) ||
+      base.decidedAt;
+    const hasVerdict = !!verdict;
+    const supportCount = typeof base.supportCount !== 'undefined' ? base.supportCount : 0;
+    return {
+      ...base,
+      id,
+      respondent: base.respondentId || base.respondent,
+      evidences,
+      answers,
+      settlements,
+      acceptedSettlements,
+      verdict,
+      decidedAt,
+      isAccuser,
+      isRespondent,
+      isMediator,
+      isJudge,
+      isDictator,
+      mine,
+      publicDetails,
+      myPublicPreference,
+      supportCount,
+      hasVerdict
+    };
+  }
+
+  return {
+    getCurrentUserId,
+    openCase,
+    listCases,
+    listCasesForUser,
+    getCaseById,
+    setMediators,
+    assignJudge,
+    addEvidence,
+    answerCase,
+    issueVerdict,
+    proposeSettlement,
+    acceptSettlement,
+    setPublicPreference,
+    openPopularVote,
+    nominateJudge,
+    voteNomination,
+    listNominations,
+    getCaseDetails
+  };
+};
+

+ 7 - 6
src/models/stats_model.js

@@ -36,7 +36,9 @@ module.exports = ({ cooler }) => {
   const types = [
   const types = [
     'bookmark','event','task','votes','report','feed','project',
     'bookmark','event','task','votes','report','feed','project',
     'image','audio','video','document','transfer','post','tribe',
     'image','audio','video','document','transfer','post','tribe',
-    'market','forum','job','aiExchange'
+    'market','forum','job','aiExchange',
+    'parliamentCandidature','parliamentTerm','parliamentProposal','parliamentRevocation','parliamentLaw',
+    'courtsCase','courtsEvidence','courtsAnswer','courtsVerdict','courtsSettlement','courtsSettlementProposal','courtsSettlementAccepted','courtsNomination','courtsNominationVote'
   ];
   ];
 
 
   const getFolderSize = (folderPath) => {
   const getFolderSize = (folderPath) => {
@@ -141,10 +143,10 @@ module.exports = ({ cooler }) => {
     for (const m of scopedMsgs) {
     for (const m of scopedMsgs) {
       const k = m.key;
       const k = m.key;
       const c = m.value.content;
       const c = m.value.content;
-      const t = c.type;
-      if (!types.includes(t)) continue;
-      byType[t].set(k, { key: k, ts: m.value.timestamp, content: c, author: m.value.author });
-      if (c.replaces) parentOf[t].set(k, c.replaces);
+      theType = c.type;
+      if (!types.includes(theType)) continue;
+      byType[theType].set(k, { key: k, ts: m.value.timestamp, content: c, author: m.value.author });
+      if (c.replaces) parentOf[theType].set(k, c.replaces);
     }
     }
 
 
     const findRoot = (t, id) => {
     const findRoot = (t, id) => {
@@ -198,7 +200,6 @@ module.exports = ({ cooler }) => {
     const content = {};
     const content = {};
     const opinions = {};
     const opinions = {};
     for (const t of types) {
     for (const t of types) {
-      if (t === 'karmaScore') continue;
       let vals;
       let vals;
       if (t === 'tribe') {
       if (t === 'tribe') {
         vals = tribeDedupContents;
         vals = tribeDedupContents;

+ 1 - 1
src/server/package-lock.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@krakenslab/oasis",
   "name": "@krakenslab/oasis",
-  "version": "0.5.4",
+  "version": "0.5.5",
   "lockfileVersion": 3,
   "lockfileVersion": 3,
   "requires": true,
   "requires": true,
   "packages": {
   "packages": {

+ 1 - 1
src/server/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@krakenslab/oasis",
   "name": "@krakenslab/oasis",
-  "version": "0.5.4",
+  "version": "0.5.5",
   "description": "Oasis Social Networking Project Utopia",
   "description": "Oasis Social Networking Project Utopia",
   "repository": {
   "repository": {
     "type": "git",
     "type": "git",

+ 209 - 130
src/views/activity_view.js

@@ -71,17 +71,26 @@ function renderActionCards(actions, userId) {
 
 
   const seenDocumentTitles = new Set();
   const seenDocumentTitles = new Set();
 
 
-  return deduped.map(action => {
+  const cards = deduped.map(action => {
     const date = action.ts ? new Date(action.ts).toLocaleString() : "";
     const date = action.ts ? new Date(action.ts).toLocaleString() : "";
     const userLink = action.author
     const userLink = action.author
       ? a({ href: `/author/${encodeURIComponent(action.author)}` }, action.author)
       ? a({ href: `/author/${encodeURIComponent(action.author)}` }, action.author)
       : 'unknown';
       : 'unknown';
     const type = action.type || 'unknown';
     const type = action.type || 'unknown';
+    let skip = false;
 
 
     let headerText;
     let headerText;
     if (type.startsWith('parliament')) {
     if (type.startsWith('parliament')) {
       const sub = type.replace('parliament', '');
       const sub = type.replace('parliament', '');
       headerText = `[PARLIAMENT · ${sub.toUpperCase()}]`;
       headerText = `[PARLIAMENT · ${sub.toUpperCase()}]`;
+    } else if (type.startsWith('courts')) {
+      const rawSub = type.slice('courts'.length);
+      const pretty = rawSub
+        .replace(/^[_\s]+/, '')
+        .replace(/[_\s]+/g, ' · ')
+        .replace(/([a-z])([A-Z])/g, '$1 · $2');
+      const finalSub = pretty || 'EVENT';
+      headerText = `[COURTS · ${finalSub.toUpperCase()}]`;
     } else {
     } else {
       const typeLabel = i18n[`type${capitalize(type)}`] || type;
       const typeLabel = i18n[`type${capitalize(type)}`] || type;
       headerText = `[${String(typeLabel).toUpperCase()}]`;
       headerText = `[${String(typeLabel).toUpperCase()}]`;
@@ -180,14 +189,13 @@ function renderActionCards(actions, userId) {
             location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.tribeLocationLabel.toUpperCase()) + ':'), span({ class: 'card-value' }, ...renderUrl(location))) : "",
             location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.tribeLocationLabel.toUpperCase()) + ':'), span({ class: 'card-value' }, ...renderUrl(location))) : "",
             typeof isAnonymous === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeIsAnonymousLabel+ ':'), span({ class: 'card-value' }, isAnonymous ? i18n.tribePrivate : i18n.tribePublic)) : "",
             typeof isAnonymous === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeIsAnonymousLabel+ ':'), span({ class: 'card-value' }, isAnonymous ? i18n.tribePrivate : i18n.tribePublic)) : "",
             inviteMode ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.tribeModeLabel) + ':'), span({ class: 'card-value' }, inviteMode.toUpperCase())) : "",
             inviteMode ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.tribeModeLabel) + ':'), span({ class: 'card-value' }, inviteMode.toUpperCase())) : "",
-            typeof isLARP === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeLARPLabel+ ':'), span({ class: 'card-value' }, isLARP ? i18n.tribeYes : i18n.tribeNo)) : "",
+            typeof isLARP === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeLARPLabel+ ':'), span({ class: 'card-value' }, isLARP ? i18n.tribeYes : i18n.tribeNo)) : ""
          ), 
          ), 
           Array.isArray(members) ? h2(`${i18n.tribeMembersCount}: ${members.length}`) : "",
           Array.isArray(members) ? h2(`${i18n.tribeMembersCount}: ${members.length}`) : "",
           image  
           image  
             ? img({ src: `/blob/${encodeURIComponent(image)}`, class: 'feed-image tribe-image' })
             ? img({ src: `/blob/${encodeURIComponent(image)}`, class: 'feed-image tribe-image' })
             : img({ src: '/assets/images/default-tribe.png', class: 'feed-image tribe-image' }),
             : img({ src: '/assets/images/default-tribe.png', class: 'feed-image tribe-image' }),
           p({ class: 'tribe-description' }, ...renderUrl(description || '')),
           p({ class: 'tribe-description' }, ...renderUrl(description || '')),
-         
           validTags.length
           validTags.length
             ? div({ class: 'card-tags' }, validTags.map(tag =>
             ? div({ class: 'card-tags' }, validTags.map(tag =>
               a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)))
               a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)))
@@ -333,7 +341,7 @@ function renderActionCards(actions, userId) {
           location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.location || 'Location') + ':'), span({ class: 'card-value' }, location)) : "",
           location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.location || 'Location') + ':'), span({ class: 'card-value' }, location)) : "",
           typeof isPublic === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.isPublic || 'Public') + ':'), span({ class: 'card-value' }, isPublic ? 'Yes' : 'No')) : "",
           typeof isPublic === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.isPublic || 'Public') + ':'), span({ class: 'card-value' }, isPublic ? 'Yes' : 'No')) : "",
           price ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.price || 'Price') + ':'), span({ class: 'card-value' }, price + " ECO")) : "",
           price ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.price || 'Price') + ':'), span({ class: 'card-value' }, price + " ECO")) : "",
-          br,
+          br(),
           organizer ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.organizer || 'Organizer') + ': '), a({ class: "user-link", href: `/author/${encodeURIComponent(organizer)}` }, organizer)) : "",
           organizer ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.organizer || 'Organizer') + ': '), a({ class: "user-link", href: `/author/${encodeURIComponent(organizer)}` }, organizer)) : "",
           Array.isArray(attendees) ? h2({ class: 'card-label' }, (i18n.attendees || 'Attendees') + ': ' + attendees.length) : ""
           Array.isArray(attendees) ? h2({ class: 'card-label' }, (i18n.attendees || 'Attendees') + ': ' + attendees.length) : ""
         )
         )
@@ -463,19 +471,18 @@ function renderActionCards(actions, userId) {
           div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStatus + ": " ), span({ class: 'card-value' }, status.toUpperCase())),
           div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStatus + ": " ), span({ class: 'card-value' }, status.toUpperCase())),
           div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : "")),
           div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : "")),
           div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStock + ':'), span({ class: 'card-value' }, stock)),
           div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStock + ':'), span({ class: 'card-value' }, stock)),
-          br,
+          br(),
           image
           image
             ? img({ src: `/blob/${encodeURIComponent(image)}` })
             ? img({ src: `/blob/${encodeURIComponent(image)}` })
             : img({ src: '/assets/images/default-market.png', alt: title }),
             : img({ src: '/assets/images/default-market.png', alt: title }),
-          br,
+          br(),
           div({ class: "market-card price" },
           div({ class: "market-card price" },
             p(`${i18n.marketItemPrice}: ${price} ECO`)
             p(`${i18n.marketItemPrice}: ${price} ECO`)
           ),
           ),
           item_type === 'auction' && status !== 'SOLD' && status !== 'DISCARDED' && !isSeller
           item_type === 'auction' && status !== 'SOLD' && status !== 'DISCARDED' && !isSeller
             ? div({ class: "auction-info" },
             ? div({ class: "auction-info" },
                 auctions_poll && auctions_poll.length > 0
                 auctions_poll && auctions_poll.length > 0
-                  ? [
-                      p({ class: "auction-bid-text" }, i18n.marketAuctionBids),
+                  ?
                       table({ class: 'auction-bid-table' },
                       table({ class: 'auction-bid-table' },
                         tr(
                         tr(
                           th(i18n.marketAuctionBidTime),
                           th(i18n.marketAuctionBidTime),
@@ -491,7 +498,6 @@ function renderActionCards(actions, userId) {
                           );
                           );
                         })
                         })
                       )
                       )
-                    ]
                   : p(i18n.marketNoBids),
                   : p(i18n.marketNoBids),
                 form({ method: "POST", action: `/market/bid/${encodeURIComponent(action.id)}` },
                 form({ method: "POST", action: `/market/bid/${encodeURIComponent(action.id)}` },
                   input({ type: "number", name: "bidAmount", step: "0.000001", min: "0.000001", placeholder: i18n.marketYourBid, required: true }),
                   input({ type: "number", name: "bidAmount", step: "0.000001", min: "0.000001", placeholder: i18n.marketYourBid, required: true }),
@@ -525,102 +531,102 @@ function renderActionCards(actions, userId) {
         deadline, followers, backers, milestones,
         deadline, followers, backers, milestones,
         bounty, bountyAmount, bounty_currency,
         bounty, bountyAmount, bounty_currency,
         activity, activityActor
         activity, activityActor
-    } = content;
-
-    const ratio = goal ? Math.min(100, Math.round((parseFloat(pledged || 0) / parseFloat(goal)) * 100)) : 0;
-    const displayStatus = String(status || 'ACTIVE').toUpperCase();
-    const followersCount = Array.isArray(followers) ? followers.length : 0;
-    const backersCount = Array.isArray(backers) ? backers.length : 0;
-    const backersTotal = sumAmounts(backers || []);
-    const msCount = Array.isArray(milestones) ? milestones.length : 0;
-    const lastMs = Array.isArray(milestones) && milestones.length ? milestones[milestones.length - 1] : null;
-    const bountyVal = typeof bountyAmount !== 'undefined'
+      } = content;
+
+      const ratio = goal ? Math.min(100, Math.round((parseFloat(pledged || 0) / parseFloat(goal)) * 100)) : 0;
+      const displayStatus = String(status || 'ACTIVE').toUpperCase();
+      const followersCount = Array.isArray(followers) ? followers.length : 0;
+      const backersCount = Array.isArray(backers) ? backers.length : 0;
+      const backersTotal = sumAmounts(backers || []);
+      const msCount = Array.isArray(milestones) ? milestones.length : 0;
+      const lastMs = Array.isArray(milestones) && milestones.length ? milestones[milestones.length - 1] : null;
+      const bountyVal = typeof bountyAmount !== 'undefined'
         ? bountyAmount
         ? bountyAmount
         : (typeof bounty === 'number' ? bounty : null);
         : (typeof bounty === 'number' ? bounty : null);
 
 
-    if (activity && activity.kind) {
+      if (activity && activity.kind) {
         const tmpl =
         const tmpl =
-            activity.kind === 'follow'
-                ? (i18n.activityProjectFollow || '%OASIS% is now %ACTION% this project: %PROJECT%')
-                : activity.kind === 'unfollow'
-                    ? (i18n.activityProjectUnfollow || '%OASIS% is now %ACTION% this project: %PROJECT%')
-                    : '%OASIS% performed an unknown action on %PROJECT%';
+          activity.kind === 'follow'
+            ? (i18n.activityProjectFollow || '%OASIS% is now %ACTION% this project: %PROJECT%')
+            : activity.kind === 'unfollow'
+              ? (i18n.activityProjectUnfollow || '%OASIS% is now %ACTION% this project: %PROJECT%')
+              : '%OASIS% performed an unknown action on %PROJECT%';
 
 
         const actionWord =
         const actionWord =
-            activity.kind === 'follow'
-                ? (i18n.following || 'FOLLOWING')
-                : activity.kind === 'unfollow'
-                    ? (i18n.unfollowing || 'UNFOLLOWING')
-                    : 'ACTION';
+          activity.kind === 'follow'
+            ? (i18n.following || 'FOLLOWING')
+            : activity.kind === 'unfollow'
+              ? (i18n.unfollowing || 'UNFOLLOWING')
+              : 'ACTION';
 
 
         const msgHtml = tmpl
         const msgHtml = tmpl
-            .replace('%OASIS%', `<a class="user-link" href="/author/${encodeURIComponent(activity.activityActor || '')}">${activity.activityActor || ''}</a>`)
-            .replace('%PROJECT%', `<a class="user-link" href="/projects/${encodeURIComponent(action.tipId || action.id)}">${title || ''}</a>`)
-            .replace('%ACTION%', `<strong>${actionWord}</strong>`);
+          .replace('%OASIS%', `<a class="user-link" href="/author/${encodeURIComponent(activity.activityActor || '')}">${activity.activityActor || ''}</a>`)
+          .replace('%PROJECT%', `<a class="user-link" href="/projects/${encodeURIComponent(action.tipId || action.id)}">${title || ''}</a>`)
+          .replace('%ACTION%', `<strong>${actionWord}</strong>`);
 
 
         return div({ class: 'card card-rpg' },
         return div({ class: 'card card-rpg' },
-            div({ class: 'card-header' },
-                h2({ class: 'card-label' }, `[${(i18n.typeProject || 'PROJECT').toUpperCase()}]`),
-                form({ method: "GET", action: `/projects/${encodeURIComponent(action.tipId || action.id)}` },
-                    button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-                )
-            ),
-            div(
-                p({ innerHTML: msgHtml })
-            ),
-            p({ class: 'card-footer' },
-                span({ class: 'date-link' }, `${action.ts ? new Date(action.ts).toLocaleString() : ''} ${i18n.performed} `),
-                a({ href: `/author/${encodeURIComponent(action.author)}`, class: 'user-link' }, `${action.author}`)
+          div({ class: 'card-header' },
+            h2({ class: 'card-label' }, `[${(i18n.typeProject || 'PROJECT').toUpperCase()}]`),
+            form({ method: "GET", action: `/projects/${encodeURIComponent(action.tipId || action.id)}` },
+              button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
             )
             )
+          ),
+          div(
+            p({ innerHTML: msgHtml })
+          ),
+          p({ class: 'card-footer' },
+            span({ class: 'date-link' }, `${action.ts ? new Date(action.ts).toLocaleString() : ''} ${i18n.performed} `),
+            a({ href: `/author/${encodeURIComponent(action.author)}`, class: 'user-link' }, `${action.author}`)
+          )
         );
         );
-    }
+      }
 
 
-    cardBody.push(
+      cardBody.push(
         div({ class: 'card-section project' },
         div({ class: 'card-section project' },
-            title ? div({ class: 'card-field' },
-                span({ class: 'card-label' }, i18n.title + ':'),
-                span({ class: 'card-value' }, title)
-            ) : "",
-            typeof goal !== 'undefined' ? div({ class: 'card-field' },
-                span({ class: 'card-label' }, i18n.projectGoal + ':'),
-                span({ class: 'card-value' }, `${goal} ECO`)
-            ) : "",
-            typeof progress !== 'undefined' ? div({ class: 'card-field' },
-                span({ class: 'card-label' }, i18n.projectProgress + ':'),
-                span({ class: 'card-value' }, `${progress || 0}%`)
-            ) : "",
-            deadline ? div({ class: 'card-field' },
-                span({ class: 'card-label' }, i18n.projectDeadline + ':'),
-                span({ class: 'card-value' }, moment(deadline).format('YYYY/MM/DD HH:mm'))
-            ) : "",
-            div({ class: 'card-field' },
-                span({ class: 'card-label' }, i18n.projectStatus + ':'),
-                span({ class: 'card-value' }, i18n['projectStatus' + displayStatus] || displayStatus)
-            ),
-            div({ class: 'card-field' },
-                span({ class: 'card-label' }, i18n.projectFunding + ':'),
-                span({ class: 'card-value' }, `${ratio}%`)
-            ),
-            typeof pledged !== 'undefined' ? div({ class: 'card-field' },
-                span({ class: 'card-label' }, i18n.projectPledged + ':'),
-                span({ class: 'card-value' }, `${pledged || 0} ECO`)
-            ) : "",
-            div({ class: 'card-field' },
-                span({ class: 'card-label' }, i18n.projectFollowers + ':'),
-                span({ class: 'card-value' }, `${followersCount}`)
-            ),
-            div({ class: 'card-field' },
-                span({ class: 'card-label' }, i18n.projectBackers + ':'),
-                span({ class: 'card-value' }, `${backersCount} · ${backersTotal} ECO`)
-            ),
-            msCount ? div({ class: 'card-field' },
-                span({ class: 'card-label' }, (i18n.projectMilestones || 'Milestones') + ':'),
-                span({ class: 'card-value' }, `${msCount}${lastMs && lastMs.title ? ' · ' + lastMs.title : ''}`)
-            ) : "",
-            bountyVal != null ? div({ class: 'card-field' },
-                span({ class: 'card-label' }, (i18n.projectBounty || 'Bounty') + ':'),
-                span({ class: 'card-value' }, `${bountyVal} ${(bounty_currency || 'ECO').toUpperCase()}`)
-            ) : ""
+          title ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.title + ':'),
+            span({ class: 'card-value' }, title)
+          ) : "",
+          typeof goal !== 'undefined' ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.projectGoal + ':'),
+            span({ class: 'card-value' }, `${goal} ECO`)
+          ) : "",
+          typeof progress !== 'undefined' ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.projectProgress + ':'),
+            span({ class: 'card-value' }, `${progress || 0}%`)
+          ) : "",
+          deadline ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.projectDeadline + ':'),
+            span({ class: 'card-value' }, moment(deadline).format('YYYY/MM/DD HH:mm'))
+          ) : "",
+          div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.projectStatus + ':'),
+            span({ class: 'card-value' }, i18n['projectStatus' + displayStatus] || displayStatus)
+          ),
+          div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.projectFunding + ':'),
+            span({ class: 'card-value' }, `${ratio}%`)
+          ),
+          typeof pledged !== 'undefined' ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.projectPledged + ':'),
+            span({ class: 'card-value' }, `${pledged || 0} ECO`)
+          ) : "",
+          div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.projectFollowers + ':'),
+            span({ class: 'card-value' }, `${followersCount}`)
+          ),
+          div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.projectBackers + ':'),
+            span({ class: 'card-value' }, `${backersCount} · ${backersTotal} ECO`)
+          ),
+          msCount ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, (i18n.projectMilestones || 'Milestones') + ':'),
+            span({ class: 'card-value' }, `${msCount}${lastMs && lastMs.title ? ' · ' + lastMs.title : ''}`)
+          ) : "",
+          bountyVal != null ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, (i18n.projectBounty || 'Bounty') + ':'),
+            span({ class: 'card-value' }, `${bountyVal} ${(bounty_currency || 'ECO').toUpperCase()}`)
+          ) : ""
         )
         )
       );
       );
     }
     }
@@ -792,14 +798,58 @@ function renderActionCards(actions, userId) {
       );
       );
     }
     }
 
 
+    if (type.startsWith('courts')) {
+      if (type === 'courtsCase') {
+        const { title, method, accuser, status, answerBy, evidenceBy, decisionBy, needed, yes, total, voteId } = content;
+        cardBody.push(
+          div({ class: 'card-section courts' },
+            title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsCaseTitle.toUpperCase() + ':'), span({ class: 'card-value' }, title)) : '',
+            status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThStatus.toUpperCase() + ':'), span({ class: 'card-value' }, status)) : '',
+            method ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsMethod.toUpperCase() + ':'), span({ class: 'card-value' }, String(i18n['courtsMethod' + String(method).toUpperCase()] || method).toUpperCase())) : '',
+            answerBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThAnswerBy + ':'), span({ class: 'card-value' }, new Date(answerBy).toLocaleString())) : '',
+            evidenceBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThEvidenceBy + ':'), span({ class: 'card-value' }, new Date(evidenceBy).toLocaleString())) : '',
+            decisionBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThDecisionBy + ':'), span({ class: 'card-value' }, new Date(decisionBy).toLocaleString())) : '',
+            accuser ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsAccuser + ':'), span({ class: 'card-value' }, a({ href: `/author/${encodeURIComponent(accuser)}`, class: 'user-link' }, accuser))) : '',
+            typeof needed !== 'undefined' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsVotesNeeded + ':'), span({ class: 'card-value' }, String(needed))) : '',
+            (typeof yes !== 'undefined' || typeof total !== 'undefined') ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsVotesSlashTotal + ':'), span({ class: 'card-value' }, `${Number(yes || 0)}/${Number(total || 0)}`)) : '',
+            voteId ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsOpenVote + ':'), a({ href: `/votes/${encodeURIComponent(voteId)}`, class: 'tag-link' }, i18n.viewDetails || 'View details')) : ''
+          )
+        );
+      } else if (type === 'courtsNomination') {
+        const { judgeId } = content;
+        cardBody.push(
+          div({ class: 'card-section courts' },
+            judgeId ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsJudge + ':'), span({ class: 'card-value' }, a({ href: `/author/${encodeURIComponent(judgeId)}`, class: 'user-link' }, judgeId))) : ''
+          )
+        );
+      } else {
+        skip = true;
+      }
+    }
+
     const viewHref = getViewDetailsAction(type, action);
     const viewHref = getViewDetailsAction(type, action);
     const isParliamentTarget =
     const isParliamentTarget =
       viewHref === '/parliament?filter=candidatures' ||
       viewHref === '/parliament?filter=candidatures' ||
       viewHref === '/parliament?filter=government'   ||
       viewHref === '/parliament?filter=government'   ||
       viewHref === '/parliament?filter=proposals'    ||
       viewHref === '/parliament?filter=proposals'    ||
-      viewHref === '/parliament?filter=revocations'    ||
+      viewHref === '/parliament?filter=revocations'  ||
       viewHref === '/parliament?filter=laws';
       viewHref === '/parliament?filter=laws';
+
+    const isCourtsTarget =
+      viewHref === '/courts?filter=cases'    ||
+      viewHref === '/courts?filter=mycases'  ||
+      viewHref === '/courts?filter=actions'  ||
+      viewHref === '/courts?filter=judges'   ||
+      viewHref === '/courts?filter=history'  ||
+      viewHref === '/courts?filter=rules'    ||
+      viewHref === '/courts?filter=open';
+
     const parliamentFilter = isParliamentTarget ? (viewHref.split('filter=')[1] || '') : '';
     const parliamentFilter = isParliamentTarget ? (viewHref.split('filter=')[1] || '') : '';
+    const courtsFilter     = isCourtsTarget     ? (viewHref.split('filter=')[1] || '') : '';
+
+    if (skip) {
+      return null;
+    }
 
 
     return div({ class: 'card card-rpg' },
     return div({ class: 'card card-rpg' },
       div({ class: 'card-header' },
       div({ class: 'card-header' },
@@ -807,13 +857,21 @@ function renderActionCards(actions, userId) {
         type !== 'feed' && type !== 'aiExchange' && type !== 'bankWallet' && (!action.tipId || action.tipId === action.id)
         type !== 'feed' && type !== 'aiExchange' && type !== 'bankWallet' && (!action.tipId || action.tipId === action.id)
           ? (
           ? (
               isParliamentTarget
               isParliamentTarget
-                ? form({ method: "GET", action: "/parliament" },
+                ? form(
+                    { method: "GET", action: "/parliament" },
                     input({ type: "hidden", name: "filter", value: parliamentFilter }),
                     input({ type: "hidden", name: "filter", value: parliamentFilter }),
                     button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
                     button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
                   )
                   )
-                : form({ method: "GET", action: viewHref },
-                    button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-                  )
+                : isCourtsTarget
+                  ? form(
+                      { method: "GET", action: "/courts" },
+                      input({ type: "hidden", name: "filter", value: courtsFilter }),
+                      button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
+                    )
+                  : form(
+                      { method: "GET", action: viewHref },
+                      button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
+                    )
             )
             )
           : ''
           : ''
       ),
       ),
@@ -824,43 +882,58 @@ function renderActionCards(actions, userId) {
       )
       )
     );
     );
   });
   });
+
+  const filteredCards = cards.filter(Boolean);
+  if (!filteredCards.length) {
+    return div({ class: "no-actions" }, p(i18n.noActions));
+  }
+  return filteredCards;
 }
 }
 
 
 function getViewDetailsAction(type, action) {
 function getViewDetailsAction(type, action) {
-    const id = encodeURIComponent(action.tipId || action.id);
-    switch (type) {
-        case 'parliamentCandidature': return `/parliament?filter=candidatures`;
-        case 'parliamentTerm':        return `/parliament?filter=government`;
-        case 'parliamentProposal':    return `/parliament?filter=proposals`;
-        case 'parliamentRevocation':  return `/parliament?filter=revocations`;
-        case 'parliamentLaw':         return `/parliament?filter=laws`;
-        case 'votes':      return `/votes/${id}`;
-        case 'transfer':   return `/transfers/${id}`;
-        case 'pixelia':    return `/pixelia`;
-        case 'tribe':      return `/tribe/${id}`;
-        case 'curriculum': return `/inhabitant/${encodeURIComponent(action.author)}`;
-        case 'karmaScore': return `/author/${encodeURIComponent(action.author)}`;
-        case 'image':      return `/images/${id}`;
-        case 'audio':      return `/audios/${id}`;
-        case 'video':      return `/videos/${id}`;
-        case 'forum':      return `/forum/${encodeURIComponent(action.content?.key || action.tipId || action.id)}`;
-        case 'document':   return `/documents/${id}`;
-        case 'bookmark':   return `/bookmarks/${id}`;
-        case 'event':      return `/events/${id}`;
-        case 'task':       return `/tasks/${id}`;
-        case 'about':      return `/author/${encodeURIComponent(action.author)}`;
-        case 'post':       return `/thread/${id}#${id}`;
-        case 'vote':       return `/thread/${encodeURIComponent(action.content.vote.link)}#${encodeURIComponent(action.content.vote.link)}`;
-        case 'contact':    return `/inhabitants`;
-        case 'pub':        return `/invites`;
-        case 'market':     return `/market/${id}`;
-        case 'job':        return `/jobs/${id}`;
-        case 'project':    return `/projects/${id}`;
-        case 'report':     return `/reports/${id}`;
-        case 'bankWallet': return `/wallet`;
-        case 'bankClaim':  return `/banking${action.content?.epochId ? `/epoch/${encodeURIComponent(action.content.epochId)}` : ''}`;
-        default:           return `/activity`;
-    }
+  const id = encodeURIComponent(action.tipId || action.id);
+  switch (type) {
+    case 'parliamentCandidature':   return `/parliament?filter=candidatures`;
+    case 'parliamentTerm':          return `/parliament?filter=government`;
+    case 'parliamentProposal':      return `/parliament?filter=proposals`;
+    case 'parliamentRevocation':    return `/parliament?filter=revocations`;
+    case 'parliamentLaw':           return `/parliament?filter=laws`;
+    case 'courtsCase':              return `/courts/cases/${encodeURIComponent(action.id)}`;
+    case 'courtsEvidence':          return `/courts?filter=actions`;
+    case 'courtsAnswer':            return `/courts?filter=actions`;
+    case 'courtsVerdict':           return `/courts?filter=actions`;
+    case 'courtsSettlement':        return `/courts?filter=actions`;
+    case 'courtsSettlementProposal':return `/courts?filter=actions`;
+    case 'courtsSettlementAccepted':return `/courts?filter=actions`;
+    case 'courtsNomination':        return `/courts?filter=judges`;
+    case 'courtsNominationVote':    return `/courts?filter=judges`;
+    case 'votes':      return `/votes/${id}`;
+    case 'transfer':   return `/transfers/${id}`;
+    case 'pixelia':    return `/pixelia`;
+    case 'tribe':      return `/tribe/${id}`;
+    case 'curriculum': return `/inhabitant/${encodeURIComponent(action.author)}`;
+    case 'karmaScore': return `/author/${encodeURIComponent(action.author)}`;
+    case 'image':      return `/images/${id}`;
+    case 'audio':      return `/audios/${id}`;
+    case 'video':      return `/videos/${id}`;
+    case 'forum':      return `/forum/${encodeURIComponent(action.content?.key || action.tipId || action.id)}`;
+    case 'document':   return `/documents/${id}`;
+    case 'bookmark':   return `/bookmarks/${id}`;
+    case 'event':      return `/events/${id}`;
+    case 'task':       return `/tasks/${id}`;
+    case 'about':      return `/author/${encodeURIComponent(action.author)}`;
+    case 'post':       return `/thread/${id}#${id}`;
+    case 'vote':       return `/thread/${encodeURIComponent(action.content.vote.link)}#${encodeURIComponent(action.content.vote.link)}`;
+    case 'contact':    return `/inhabitants`;
+    case 'pub':        return `/invites`;
+    case 'market':     return `/market/${id}`;
+    case 'job':        return `/jobs/${id}`;
+    case 'project':    return `/projects/${id}`;
+    case 'report':     return `/reports/${id}`;
+    case 'bankWallet': return `/wallet`;
+    case 'bankClaim':  return `/banking${action.content?.epochId ? `/epoch/${encodeURIComponent(action.content.epochId)}` : ''}`;
+    default:           return `/activity`;
+  }
 }
 }
 
 
 exports.activityView = (actions, filter, userId) => {
 exports.activityView = (actions, filter, userId) => {
@@ -877,6 +950,7 @@ exports.activityView = (actions, filter, userId) => {
     { type: 'job',       label: i18n.typeJob },
     { type: 'job',       label: i18n.typeJob },
     { type: 'transfer',  label: i18n.typeTransfer },
     { type: 'transfer',  label: i18n.typeTransfer },
     { type: 'parliament',label: i18n.typeParliament },
     { type: 'parliament',label: i18n.typeParliament },
+    { type: 'courts',    label: i18n.typeCourts },
     { type: 'votes',     label: i18n.typeVotes },
     { type: 'votes',     label: i18n.typeVotes },
     { type: 'event',     label: i18n.typeEvent },
     { type: 'event',     label: i18n.typeEvent },
     { type: 'task',      label: i18n.typeTask },
     { type: 'task',      label: i18n.typeTask },
@@ -907,6 +981,11 @@ exports.activityView = (actions, filter, userId) => {
     filteredActions = actions.filter(action => action.type !== 'tombstone' && (action.type === 'bankWallet' || action.type === 'bankClaim'));
     filteredActions = actions.filter(action => action.type !== 'tombstone' && (action.type === 'bankWallet' || action.type === 'bankClaim'));
   } else if (filter === 'parliament') {
   } else if (filter === 'parliament') {
     filteredActions = actions.filter(action => ['parliamentCandidature','parliamentTerm','parliamentProposal','parliamentRevocation','parliamentLaw'].includes(action.type));
     filteredActions = actions.filter(action => ['parliamentCandidature','parliamentTerm','parliamentProposal','parliamentRevocation','parliamentLaw'].includes(action.type));
+  } else if (filter === 'courts') {
+    filteredActions = actions.filter(action => {
+      const t = String(action.type || '').toLowerCase();
+      return t === 'courtscase' || t === 'courtsnomination' || t === 'courtsnominationvote';
+    });
   } else {
   } else {
     filteredActions = actions.filter(action => (action.type === filter || filter === 'all') && action.type !== 'tombstone');
     filteredActions = actions.filter(action => (action.type === filter || filter === 'all') && action.type !== 'tombstone');
   }
   }

+ 15 - 2
src/views/blockchain_view.js

@@ -11,11 +11,11 @@ const FILTER_LABELS = {
   forum: i18n.typeForum, about: i18n.typeAbout, contact: i18n.typeContact, pub: i18n.typePub,
   forum: i18n.typeForum, about: i18n.typeAbout, contact: i18n.typeContact, pub: i18n.typePub,
   transfer: i18n.typeTransfer, market: i18n.typeMarket, job: i18n.typeJob, tribe: i18n.typeTribe,
   transfer: i18n.typeTransfer, market: i18n.typeMarket, job: i18n.typeJob, tribe: i18n.typeTribe,
   project: i18n.typeProject, banking: i18n.typeBanking, bankWallet: i18n.typeBankWallet, bankClaim: i18n.typeBankClaim,
   project: i18n.typeProject, banking: i18n.typeBanking, bankWallet: i18n.typeBankWallet, bankClaim: i18n.typeBankClaim,
-  aiExchange: i18n.typeAiExchange, parliament: i18n.typeParliament
+  aiExchange: i18n.typeAiExchange, parliament: i18n.typeParliament, courts: i18n.typeCourts
 };
 };
 
 
 const BASE_FILTERS = ['recent', 'all', 'mine', 'tombstone'];
 const BASE_FILTERS = ['recent', 'all', 'mine', 'tombstone'];
-const CAT_BLOCK1  = ['votes', 'event', 'task', 'report', 'parliament'];
+const CAT_BLOCK1  = ['votes', 'event', 'task', 'report', 'parliament', 'courts'];
 const CAT_BLOCK2  = ['pub', 'tribe', 'about', 'contact', 'curriculum', 'vote', 'aiExchange'];
 const CAT_BLOCK2  = ['pub', 'tribe', 'about', 'contact', 'curriculum', 'vote', 'aiExchange'];
 const CAT_BLOCK3  = ['banking', 'job', 'market', 'project', 'transfer', 'feed', 'post', 'pixelia'];
 const CAT_BLOCK3  = ['banking', 'job', 'market', 'project', 'transfer', 'feed', 'post', 'pixelia'];
 const CAT_BLOCK4  = ['forum', 'bookmark', 'image', 'video', 'audio', 'document'];
 const CAT_BLOCK4  = ['forum', 'bookmark', 'image', 'video', 'audio', 'document'];
@@ -29,6 +29,10 @@ const filterBlocks = (blocks, filter, userId) => {
     const pset = new Set(['parliamentTerm','parliamentProposal','parliamentLaw','parliamentCandidature','parliamentRevocation']);
     const pset = new Set(['parliamentTerm','parliamentProposal','parliamentLaw','parliamentCandidature','parliamentRevocation']);
     return blocks.filter(b => pset.has(b.type));
     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));
+  }
   return blocks.filter(b => b.type === filter);
   return blocks.filter(b => b.type === filter);
 };
 };
 
 
@@ -76,6 +80,15 @@ const getViewDetailsAction = (type, block) => {
     case 'parliamentLaw': return `/parliament`;
     case 'parliamentLaw': return `/parliament`;
     case 'parliamentCandidature': return `/parliament`;
     case 'parliamentCandidature': return `/parliament`;
     case 'parliamentRevocation': 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`;
     default: return null;
     default: return null;
   }
   }
 };
 };

File diff suppressed because it is too large
+ 1395 - 0
src/views/courts_view.js


+ 10 - 0
src/views/main_views.js

@@ -283,6 +283,15 @@ const renderParliamentLink = () => {
   return parliamentMod 
   return parliamentMod 
     ? [
     ? [
         navLink({ href: "/parliament", emoji: "ꗞ", text: i18n.parliamentTitle, class: "parliament-link enabled" }),
         navLink({ href: "/parliament", emoji: "ꗞ", text: i18n.parliamentTitle, class: "parliament-link enabled" }),
+      ]
+    : '';
+};
+
+const renderCourtsLink = () => {
+  const courtsMod = getConfig().modules.courtsMod === 'on';
+  return courtsMod 
+    ? [
+        navLink({ href: "/courts", emoji: "ꖻ", text: i18n.courtsTitle, class: "courts-link enabled" }),
         hr(),
         hr(),
       ]
       ]
     : '';
     : '';
@@ -470,6 +479,7 @@ const template = (titlePrefix, ...elements) => {
               navLink({ href: "/inhabitants", emoji: "ꖘ", text: i18n.inhabitantsLabel }),
               navLink({ href: "/inhabitants", emoji: "ꖘ", text: i18n.inhabitantsLabel }),
               renderTribesLink(),
               renderTribesLink(),
               renderParliamentLink(),
               renderParliamentLink(),
+              renderCourtsLink(),
               renderVotationsLink(),
               renderVotationsLink(),
               renderEventsLink(),
               renderEventsLink(),
               renderTasksLink(),
               renderTasksLink(),

+ 1 - 0
src/views/modules_view.js

@@ -11,6 +11,7 @@ const modulesView = () => {
     { name: 'banking', label: i18n.modulesBankingLabel, description: i18n.modulesBankingDescription },
     { name: 'banking', label: i18n.modulesBankingLabel, description: i18n.modulesBankingDescription },
     { name: 'bookmarks', label: i18n.modulesBookmarksLabel, description: i18n.modulesBookmarksDescription },
     { name: 'bookmarks', label: i18n.modulesBookmarksLabel, description: i18n.modulesBookmarksDescription },
     { name: 'cipher', label: i18n.modulesCipherLabel, description: i18n.modulesCipherDescription },
     { name: 'cipher', label: i18n.modulesCipherLabel, description: i18n.modulesCipherDescription },
+    { name: 'courts', label: i18n.modulesCourtsLabel, description: i18n.modulesCourtsDescription },
     { name: 'docs', label: i18n.modulesDocsLabel, description: i18n.modulesDocsDescription },
     { name: 'docs', label: i18n.modulesDocsLabel, description: i18n.modulesDocsDescription },
     { name: 'events', label: i18n.modulesEventsLabel, description: i18n.modulesEventsDescription },
     { name: 'events', label: i18n.modulesEventsLabel, description: i18n.modulesEventsDescription },
     { name: 'feed', label: i18n.modulesFeedLabel, description: i18n.modulesFeedDescription },
     { name: 'feed', label: i18n.modulesFeedLabel, description: i18n.modulesFeedDescription },

+ 60 - 19
src/views/stats_view.js

@@ -1,6 +1,23 @@
 const { div, h2, p, section, button, form, input, ul, li, a, h3, span, strong, table, tr, td, th } = require("../server/node_modules/hyperaxe");
 const { div, h2, p, section, button, form, input, ul, li, a, h3, span, strong, table, tr, td, th } = require("../server/node_modules/hyperaxe");
 const { template, i18n } = require('./main_views');
 const { template, i18n } = require('./main_views');
 
 
+Object.assign(i18n, {
+  statsParliamentCandidature: "Parliament candidatures",
+  statsParliamentTerm: "Parliament terms",
+  statsParliamentProposal: "Parliament proposals",
+  statsParliamentRevocation: "Parliament revocations",
+  statsParliamentLaw: "Parliament laws",
+  statsCourtsCase: "Court cases",
+  statsCourtsEvidence: "Court evidence",
+  statsCourtsAnswer: "Court answers",
+  statsCourtsVerdict: "Court verdicts",
+  statsCourtsSettlement: "Court settlements",
+  statsCourtsSettlementProposal: "Settlement proposals",
+  statsCourtsSettlementAccepted: "Settlements accepted",
+  statsCourtsNomination: "Judge nominations",
+  statsCourtsNominationVote: "Nomination votes"
+});
+
 const C = (stats, t) => Number((stats && stats.content && stats.content[t]) || 0);
 const C = (stats, t) => Number((stats && stats.content && stats.content[t]) || 0);
 const O = (stats, t) => Number((stats && stats.opinions && stats.opinions[t]) || 0);
 const O = (stats, t) => Number((stats && stats.opinions && stats.opinions[t]) || 0);
 
 
@@ -11,8 +28,44 @@ exports.statsView = (stats, filter) => {
   const types = [
   const types = [
     'bookmark', 'event', 'task', 'votes', 'report', 'feed', 'project',
     'bookmark', 'event', 'task', 'votes', 'report', 'feed', 'project',
     'image', 'audio', 'video', 'document', 'transfer', 'post', 'tribe',
     'image', 'audio', 'video', 'document', 'transfer', 'post', 'tribe',
-    'market', 'forum', 'job', 'aiExchange'
+    'market', 'forum', 'job', 'aiExchange',
+    'parliamentCandidature','parliamentTerm','parliamentProposal','parliamentRevocation','parliamentLaw',
+    'courtsCase','courtsEvidence','courtsAnswer','courtsVerdict','courtsSettlement','courtsSettlementProposal','courtsSettlementAccepted','courtsNomination','courtsNominationVote'
   ];
   ];
+  const labels = {
+    bookmark: i18n.statsBookmark,
+    event: i18n.statsEvent,
+    task: i18n.statsTask,
+    votes: i18n.statsVotes,
+    report: i18n.statsReport,
+    feed: i18n.statsFeed,
+    project: i18n.statsProject,
+    image: i18n.statsImage,
+    audio: i18n.statsAudio,
+    video: i18n.statsVideo,
+    document: i18n.statsDocument,
+    transfer: i18n.statsTransfer,
+    post: i18n.statsPost,
+    tribe: i18n.statsTribe,
+    market: i18n.statsMarket,
+    forum: i18n.statsForum,
+    job: i18n.statsJob,
+    aiExchange: i18n.statsAiExchange,
+    parliamentCandidature: i18n.statsParliamentCandidature,
+    parliamentTerm: i18n.statsParliamentTerm,
+    parliamentProposal: i18n.statsParliamentProposal,
+    parliamentRevocation: i18n.statsParliamentRevocation,
+    parliamentLaw: i18n.statsParliamentLaw,
+    courtsCase: i18n.statsCourtsCase,
+    courtsEvidence: i18n.statsCourtsEvidence,
+    courtsAnswer: i18n.statsCourtsAnswer,
+    courtsVerdict: i18n.statsCourtsVerdict,
+    courtsSettlement: i18n.statsCourtsSettlement,
+    courtsSettlementProposal: i18n.statsCourtsSettlementProposal,
+    courtsSettlementAccepted: i18n.statsCourtsSettlementAccepted,
+    courtsNomination: i18n.statsCourtsNomination,
+    courtsNominationVote: i18n.statsCourtsNominationVote
+  };
   const totalContent = types.filter(t => t !== 'karmaScore').reduce((sum, t) => sum + C(stats, t), 0);
   const totalContent = types.filter(t => t !== 'karmaScore').reduce((sum, t) => sum + C(stats, t), 0);
   const totalOpinions = types.reduce((sum, t) => sum + O(stats, t), 0);
   const totalOpinions = types.reduce((sum, t) => sum + O(stats, t), 0);
   const blockStyle = 'padding:16px;border:1px solid #ddd;border-radius:8px;margin-bottom:24px;';
   const blockStyle = 'padding:16px;border:1px solid #ddd;border-radius:8px;margin-bottom:24px;';
@@ -112,30 +165,18 @@ exports.statsView = (stats, filter) => {
                   li(`${i18n.statsProjectsActiveFundingAvg}: ${((stats.projectsKPIs?.activeFundingAvg || 0)).toFixed(1)}%`)
                   li(`${i18n.statsProjectsActiveFundingAvg}: ${((stats.projectsKPIs?.activeFundingAvg || 0)).toFixed(1)}%`)
                 ])
                 ])
               ),
               ),
-              div({ style: blockStyle },
-                h2(i18n.statsJobsTitle),
-                ul([
-                  li(`${i18n.statsJobsTotal}: ${stats.jobsKPIs?.total || 0}`),
-                  li(`${i18n.statsJobsOpen}: ${stats.jobsKPIs?.open || 0}`),
-                  li(`${i18n.statsJobsClosed}: ${stats.jobsKPIs?.closed || 0}`),
-                  li(`${i18n.statsJobsOpenVacants}: ${stats.jobsKPIs?.openVacants || 0}`),
-                  li(`${i18n.statsJobsSubscribersTotal}: ${stats.jobsKPIs?.subscribersTotal || 0}`),
-                  li(`${i18n.statsJobsAvgSalary}: ${((stats.jobsKPIs?.avgSalary || 0)).toFixed(2)} ECO`),
-                  li(`${i18n.statsJobsMedianSalary}: ${((stats.jobsKPIs?.medianSalary || 0)).toFixed(2)} ECO`)
-                ])
-              ),
               div({ style: blockStyle },
               div({ style: blockStyle },
                 h2(`${i18n.statsNetworkOpinions}: ${totalOpinions}`),
                 h2(`${i18n.statsNetworkOpinions}: ${totalOpinions}`),
-                ul(types.map(t => O(stats, t) > 0 ? li(`${i18n[`stats${t.charAt(0).toUpperCase() + t.slice(1)}`]}: ${O(stats, t)}`) : null).filter(Boolean))
+                ul(types.map(t => O(stats, t) > 0 ? li(`${labels[t]}: ${O(stats, t)}`) : null).filter(Boolean))
               ),
               ),
               div({ style: blockStyle },
               div({ style: blockStyle },
                 h2(`${i18n.statsNetworkContent}: ${totalContent}`),
                 h2(`${i18n.statsNetworkContent}: ${totalContent}`),
                 ul(
                 ul(
                   types.filter(t => t !== 'karmaScore').map(t => {
                   types.filter(t => t !== 'karmaScore').map(t => {
                     if (C(stats, t) <= 0) return null;
                     if (C(stats, t) <= 0) return null;
-                    if (t !== 'tribe') return li(`${i18n[`stats${t.charAt(0).toUpperCase() + t.slice(1)}`]}: ${C(stats, t)}`);
+                    if (t !== 'tribe') return li(`${labels[t]}: ${C(stats, t)}`);
                     return li(
                     return li(
-                      span(`${i18n[`stats${t.charAt(0).toUpperCase() + t.slice(1)}`]}: ${C(stats, t)}`),
+                      span(`${labels[t]}: ${C(stats, t)}`),
                       ul([
                       ul([
                         li(`${i18n.statsPublic}: ${stats.tribePublicCount || 0}`),
                         li(`${i18n.statsPublic}: ${stats.tribePublicCount || 0}`),
                         li(`${i18n.statsPrivate}: ${stats.tribePrivateCount || 0}`)
                         li(`${i18n.statsPrivate}: ${stats.tribePrivateCount || 0}`)
@@ -202,16 +243,16 @@ exports.statsView = (stats, filter) => {
                 ),
                 ),
                 div({ style: blockStyle },
                 div({ style: blockStyle },
                   h2(`${i18n.statsYourOpinions}: ${totalOpinions}`),
                   h2(`${i18n.statsYourOpinions}: ${totalOpinions}`),
-                  ul(types.map(t => O(stats, t) > 0 ? li(`${i18n[`stats${t.charAt(0).toUpperCase() + t.slice(1)}`]}: ${O(stats, t)}`) : null).filter(Boolean))
+                  ul(types.map(t => O(stats, t) > 0 ? li(`${labels[t]}: ${O(stats, t)}`) : null).filter(Boolean))
                 ),
                 ),
                 div({ style: blockStyle },
                 div({ style: blockStyle },
                   h2(`${i18n.statsYourContent}: ${totalContent}`),
                   h2(`${i18n.statsYourContent}: ${totalContent}`),
                   ul(
                   ul(
                     types.filter(t => t !== 'karmaScore').map(t => {
                     types.filter(t => t !== 'karmaScore').map(t => {
                       if (C(stats, t) <= 0) return null;
                       if (C(stats, t) <= 0) return null;
-                      if (t !== 'tribe') return li(`${i18n[`stats${t.charAt(0).toUpperCase() + t.slice(1)}`]}: ${C(stats, t)}`);
+                      if (t !== 'tribe') return li(`${labels[t]}: ${C(stats, t)}`);
                       return li(
                       return li(
-                        span(`${i18n[`stats${t.charAt(0).toUpperCase() + t.slice(1)}`]}: ${C(stats, t)}`),
+                        span(`${labels[t]}: ${C(stats, t)}`),
                         ul([
                         ul([
                           li(`${i18n.statsPublic}: ${stats.tribePublicCount || 0}`),
                           li(`${i18n.statsPublic}: ${stats.tribePublicCount || 0}`),
                           li(`${i18n.statsPrivate}: ${stats.tribePrivateCount || 0}`),
                           li(`${i18n.statsPrivate}: ${stats.tribePrivateCount || 0}`),