Compare commits
10 Commits
56b06c1f49
...
231ceb1da0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
231ceb1da0 | ||
| 30bb863467 | |||
| cb76f063d6 | |||
| 00635beaea | |||
| 728628b4b7 | |||
| 9679f2659b | |||
| c08e8f0b2a | |||
| c94a169c8c | |||
| 0dea400320 | |||
| 24a36627b8 |
240
index.html
240
index.html
@ -1,3 +1,4 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
@ -25,20 +26,24 @@
|
||||
certificateIssuerAddress: "FFCpiaZi31TpbYw5q5VNk8qJMeDiTLgsrE"
|
||||
}
|
||||
</script>
|
||||
<script src="scripts/lib.min.js" defer></script>
|
||||
<script src="scripts/floCrypto.min.js" defer></script>
|
||||
<script src="scripts/floBlockchainAPI.min.js" defer></script>
|
||||
<script src="scripts/compactIDB.min.js" defer></script>
|
||||
<script src="scripts/floCloudAPI.min.js" defer></script>
|
||||
<script src="scripts/floDapps.min.js" defer></script>
|
||||
<script src="scripts/btcOperator.min.js" defer></script>
|
||||
<script src="scripts/lib.js" defer></script>
|
||||
<script src="scripts/floCrypto.js" defer></script>
|
||||
<script src="scripts/floBlockchainAPI.js" defer></script>
|
||||
<script src="scripts/compactIDB.js" defer></script>
|
||||
<script src="scripts/floCloudAPI.js" defer></script>
|
||||
<script src="scripts/floDapps.js" defer></script>
|
||||
<script src="scripts/btcOperator.js" defer></script>
|
||||
<script src="https://unpkg.com/uhtml@3.0.1/es.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.0/purify.min.js"
|
||||
integrity="sha512-/hVAZO5POxCKdZMSLefw30xEVwjm94PAV9ynjskGbIpBvHO9EBplEcdUlBdCKutpZsF+La8Ag4gNrG0gAOn3Ig=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer" defer></script>
|
||||
<script src="scripts/ribc.min.js" defer></script>
|
||||
<script src="scripts/ribc.js" defer></script>
|
||||
<script id="onLoadStartUp">
|
||||
function onLoadStartUp() {
|
||||
if (!window.location.hash || window.location.hash === '#') {
|
||||
window.location.hash = '#/dashboard_page'; // or '#/landing'
|
||||
}
|
||||
|
||||
routeTo('loading')
|
||||
document.body.classList.remove('hidden')
|
||||
floDapps.setCustomPrivKeyInput(getSignedIn);
|
||||
@ -48,7 +53,7 @@
|
||||
.then(() => {
|
||||
resolve()
|
||||
setTimeout(() => {
|
||||
if (!floGlobals.loaded) {
|
||||
if (!floGlobals.loaded && window.location.hash && window.location.hash !== '#') {
|
||||
routeTo(window.location.hash);
|
||||
}
|
||||
}, 0);
|
||||
@ -88,6 +93,7 @@
|
||||
RIBC.init(floGlobals.isSubAdmin).then(result => {
|
||||
console.log(result)
|
||||
renderAllElements()
|
||||
if (!window.location.hash) window.location.hash = '#/dashboard_page'; // or '#/landing'
|
||||
routeTo(window.location.hash, { firstLoad: true })
|
||||
}).catch(error => console.error(error))
|
||||
}).catch(error => console.error(error))
|
||||
@ -3282,16 +3288,49 @@
|
||||
</sm-form>
|
||||
`)
|
||||
}
|
||||
//Fixing the earlier incomplete marking
|
||||
function markTaskAsIncomplete(e) {
|
||||
currentTask = e.target.closest('.admin-task');
|
||||
getConfirmation('Mark this task as incomplete?', { message: 'Score given to participants regarding this task will also be removed', confirmText: 'Mark as incomplete', danger: true }).then(res => {
|
||||
if (res) {
|
||||
RIBC.admin.putTaskStatus('incomplete', appState.params.id, appState.params.branch, currentTask.dataset.taskId)
|
||||
// TODO: remove task scores from intern rating
|
||||
renderBranchTasks()
|
||||
adminDataChanged();
|
||||
}
|
||||
})
|
||||
const card = e?.target?.closest?.('.admin-task');
|
||||
if (!card) return;
|
||||
currentTask = card;
|
||||
|
||||
getConfirmation('Mark this task as incomplete?', {
|
||||
message: 'Any completion scores for this task will be removed from intern ratings.',
|
||||
confirmText: 'Mark as incomplete',
|
||||
danger: true
|
||||
}).then(res => {
|
||||
if (!res) return;
|
||||
|
||||
// 1) Source of truth
|
||||
RIBC.admin.putTaskStatus('incomplete', appState.params.id, appState.params.branch, currentTask.dataset.taskId);
|
||||
|
||||
// Full task id
|
||||
const taskId = `${appState.params.id}_${appState.params.branch}_${currentTask.dataset.taskId}`;
|
||||
|
||||
// 3) Who was assigned?
|
||||
const getAssigned = RIBC.getAssignedInterns || RIBC.admin?.getAssignedInterns;
|
||||
const assignedInterns = (typeof getAssigned === 'function' ? getAssigned(taskId) : []) || [];
|
||||
const reopenedDate = Date.now();
|
||||
|
||||
// 4) Append-only reopen + remove completion + recompute rating
|
||||
assignedInterns.forEach(internId => {
|
||||
RIBC.admin.addReopenedTask(internId, taskId, { reopenedDate });
|
||||
RIBC.admin.removeCompletedTask?.(internId, taskId);
|
||||
RIBC.admin.recomputeRating?.(internId);
|
||||
});
|
||||
|
||||
// 5) Ensure it appears back in the admin's "displayed" list as pending
|
||||
const shown = new Set(RIBC.getDisplayedTasks?.() || []);
|
||||
shown.add(taskId);
|
||||
RIBC.admin.setDisplayedTasks?.(Array.from(shown));
|
||||
|
||||
// (optional safety) keep internRecord/taskStatus fully consistent
|
||||
RIBC.admin.syncInternRecordWithTaskStatus?.(taskId);
|
||||
|
||||
renderBranchTasks();
|
||||
adminDataChanged();
|
||||
notify('Task marked as incomplete', 'success');
|
||||
});
|
||||
}
|
||||
|
||||
delegate(getRef('rating_wrapper'), 'click', '.rate-intern-button', e => {
|
||||
@ -3316,31 +3355,44 @@
|
||||
const day = date.getDate();
|
||||
return `${year}-${month < 10 ? '0' : ''}${month}-${day < 10 ? '0' : ''}${day}`;
|
||||
}
|
||||
function markTaskAsCompleted() {
|
||||
getConfirmation('Mark this task as completed?', { confirmText: 'Mark as completed' }).then(res => {
|
||||
if (!res) return;
|
||||
function markTaskAsCompleted(e) {
|
||||
const card = e?.target?.closest?.('.admin-task');
|
||||
if (!card) return;
|
||||
currentTask = card;
|
||||
|
||||
RIBC.admin.putTaskStatus('completed', appState.params.id, appState.params.branch, currentTask.dataset.taskId);
|
||||
getConfirmation('Mark this task as completed?', { confirmText: 'Mark as completed' })
|
||||
.then(res => {
|
||||
if (!res) return;
|
||||
|
||||
const taskId = `${appState.params.id}_${appState.params.branch}_${currentTask.dataset.taskId}`;
|
||||
// 1) Source of truth
|
||||
RIBC.admin.putTaskStatus('completed', appState.params.id, appState.params.branch, currentTask.dataset.taskId);
|
||||
|
||||
// safer lookup before calling
|
||||
const getAssigned = RIBC.getAssignedInterns || RIBC.admin?.getAssignedInterns;
|
||||
const assignedInterns = (typeof getAssigned === 'function' ? getAssigned(taskId) : []) || [];
|
||||
const completionDate = Date.now();
|
||||
// 2) Full task id
|
||||
const taskId = `${appState.params.id}_${appState.params.branch}_${currentTask.dataset.taskId}`;
|
||||
|
||||
assignedInterns.forEach(internId => {
|
||||
RIBC.admin.addCompletedTask(internId, taskId, 0, { completionDate });
|
||||
// 3) Who was assigned?
|
||||
const getAssigned = RIBC.getAssignedInterns || RIBC.admin?.getAssignedInterns;
|
||||
const assignedInterns = (typeof getAssigned === 'function' ? getAssigned(taskId) : []) || [];
|
||||
const completionDate = Date.now();
|
||||
|
||||
// 4) Record completion for each intern + recompute rating
|
||||
assignedInterns.forEach(internId => {
|
||||
RIBC.admin.addCompletedTask(internId, taskId, 0, { completionDate });
|
||||
RIBC.admin.recomputeRating?.(internId);
|
||||
});
|
||||
|
||||
// 5) Remove from displayed "pending" list
|
||||
const filteredTasks = (RIBC.getDisplayedTasks?.() || []).filter(t => t !== taskId);
|
||||
RIBC.admin.setDisplayedTasks?.(filteredTasks);
|
||||
|
||||
// (optional safety) keep internRecord/taskStatus fully consistent
|
||||
RIBC.admin.syncInternRecordWithTaskStatus?.(taskId);
|
||||
|
||||
renderBranchTasks();
|
||||
adminDataChanged();
|
||||
notify('Task marked as completed', 'success');
|
||||
});
|
||||
|
||||
const filteredTasks = RIBC.getDisplayedTasks().filter(task => task !== taskId);
|
||||
RIBC.admin.setDisplayedTasks(filteredTasks);
|
||||
renderBranchTasks();
|
||||
adminDataChanged();
|
||||
notify('Task marked as completed', 'success');
|
||||
});
|
||||
}
|
||||
|
||||
function saveTaskChanges() {
|
||||
const changedDetails = {
|
||||
title: getRef('edit_task_title').value.trim(),
|
||||
@ -3865,7 +3917,7 @@
|
||||
Will be listed after confirmation on the blockchain.
|
||||
</p>
|
||||
</div>
|
||||
<a href=${`https://blockbook.ranchimall.net/tx/${txid}`} target="_blank" class="button button--primary">View on explorer</a>
|
||||
<a href="${floBlockchainAPI.apiURL.replace(/\/$/, '')}/tx/${txid}" target="_blank" class="button button--primary">View on explorer</a>
|
||||
</div>
|
||||
`)
|
||||
}).catch((error) => {
|
||||
@ -4246,43 +4298,97 @@
|
||||
})
|
||||
}
|
||||
floGlobals.payments = {};
|
||||
floGlobals.payer = "FThgnJLcuStugLc24FJQggmp2WgaZjrBSn";
|
||||
let fetchPaymentsState = signal('idle');
|
||||
// ---- helpers (put once, top-level) ----
|
||||
function getReceiverAddress(vout, payer) {
|
||||
if (!Array.isArray(vout)) return undefined;
|
||||
for (const o of vout) {
|
||||
const addrs = o?.scriptPubKey?.addresses || [];
|
||||
for (const a of addrs) {
|
||||
if (a && a !== payer) return a; // first non-payer output = payee
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getInputAddresses(tx) {
|
||||
const outs = [];
|
||||
for (const vin of (tx?.vin || [])) {
|
||||
const addrs = vin?.addresses || (vin?.addr ? [vin.addr] : []);
|
||||
for (const a of addrs) if (a) outs.push(a);
|
||||
}
|
||||
return outs;
|
||||
}
|
||||
|
||||
// strict: require ALL inputs come from the payer/internship distribution address
|
||||
function isFromPayer(tx, payer) {
|
||||
const ins = getInputAddresses(tx);
|
||||
return ins.length > 0 && ins.every(a => a === payer);
|
||||
}
|
||||
|
||||
function parseFloAmount(floData) {
|
||||
// Handles "send 3000 rupee#" or "Send 8000.0000000000 rupee# ..."
|
||||
const m = /send\s+([\d.]+)\s+[A-Za-z0-9#]+/i.exec(floData || "");
|
||||
return m ? parseFloat(m[1]) : 0;
|
||||
}
|
||||
|
||||
function isRupeeSend(floData) {
|
||||
return /send\s+[\d.]+\s+rupee#/i.test(floData || "");
|
||||
}
|
||||
|
||||
function fetchPayments() {
|
||||
fetchPaymentsState.value = 'fetching'
|
||||
floBlockchainAPI
|
||||
.readAllTxs("FThgnJLcuStugLc24FJQggmp2WgaZjrBSn")
|
||||
.then(({ items }) => {
|
||||
floBlockchainAPI
|
||||
.readAllTxs(floGlobals.payer)
|
||||
.then(({ items }) => {
|
||||
const internList = RIBC.getInternList() || {};
|
||||
if (!floGlobals.payments) floGlobals.payments = Object.create(null);
|
||||
|
||||
items.forEach((tx) => {
|
||||
// Errorfix 1 - Ensure tx.vout exists and has the expected structure
|
||||
if (!tx.vout || !tx.vout[0] || !tx.vout[0].scriptPubKey || !tx.vout[0].scriptPubKey.addresses) return;
|
||||
// sender must be the payer/internship distribution address
|
||||
if (!isFromPayer(tx, floGlobals.payer)) return;
|
||||
|
||||
const floId = tx.vout[0].scriptPubKey.addresses[0];
|
||||
// pick the true receiver (first non-payer vout address)
|
||||
const floId = getReceiverAddress(tx?.vout, floGlobals.payer);
|
||||
if (!floId) return;
|
||||
|
||||
//Errorfix 2 - Making sure FLO ID is in the intern list
|
||||
const internList = RIBC.getInternList();
|
||||
// Ensure internList is defined and floId exists in it
|
||||
if (!internList || !(floId in internList)) return; // check if floId is of an intern
|
||||
// only count recognized interns
|
||||
if (!internList[floId]) return;
|
||||
|
||||
if (!RIBC.getInternList()[floId]) return; // check if floId is of an intern
|
||||
const { txid, floData, time } = tx
|
||||
if (!floGlobals.payments[floId])
|
||||
floGlobals.payments[floId] = {
|
||||
total: 0,
|
||||
txs: []
|
||||
};
|
||||
const amount = parseFloat(floData.match(/([0-9]+)/)[1]) || 0; // get amount from floData
|
||||
floGlobals.payments[floId].total += amount;
|
||||
floGlobals.payments[floId].txs.push({
|
||||
txid,
|
||||
amount,
|
||||
time
|
||||
});
|
||||
// only count rupee# sends
|
||||
if (!isRupeeSend(tx.floData)) return;
|
||||
|
||||
const amount = parseFloAmount(tx.floData);
|
||||
const txid = tx.txid;
|
||||
const time = Number(tx.time) || 0;
|
||||
|
||||
// init bucket
|
||||
if (!floGlobals.payments[floId]) {
|
||||
floGlobals.payments[floId] = { total: 0, txs: [] };
|
||||
}
|
||||
|
||||
// prevent duplicates (in case readAllTxs returns same tx in multiple pages later)
|
||||
const rec = floGlobals.payments[floId];
|
||||
if (!rec._seen) rec._seen = new Set();
|
||||
if (rec._seen.has(txid)) return;
|
||||
rec._seen.add(txid);
|
||||
|
||||
rec.total += amount;
|
||||
rec.txs.push({ txid, amount, time });
|
||||
});
|
||||
fetchPaymentsState.value = 'done'
|
||||
}).catch((err) => {
|
||||
notify(err, 'error')
|
||||
fetchPaymentsState.value = 'idle'
|
||||
});
|
||||
|
||||
// sort each intern’s payments by time desc (optional)
|
||||
for (const k in floGlobals.payments) {
|
||||
floGlobals.payments[k].txs.sort((a, b) => b.time - a.time);
|
||||
}
|
||||
|
||||
fetchPaymentsState.value = 'done';
|
||||
})
|
||||
.catch((err) => {
|
||||
notify(err, 'error');
|
||||
fetchPaymentsState.value = 'idle';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
const DEFAULT = {
|
||||
blockchain: floGlobals.blockchain,
|
||||
apiURL: {
|
||||
FLO: ['https://blockbook.flocard.app/'],
|
||||
FLO: ['https://blockbook.ranchimall.net/'],
|
||||
FLO_TEST: ['https://blockbook-testnet.ranchimall.net/']
|
||||
},
|
||||
sendAmt: 0.0003,
|
||||
@ -16,6 +16,7 @@
|
||||
receiverID: floGlobals.adminID
|
||||
};
|
||||
|
||||
floBlockchainAPI.apiURL = DEFAULT.apiURL[DEFAULT.blockchain][0];
|
||||
const SATOSHI_IN_BTC = 1e8;
|
||||
const isUndefined = val => typeof val === 'undefined';
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
(function (EXPORTS) { //floCloudAPI v2.4.5
|
||||
(function (EXPORTS) { //floCloudAPI v2.4.5a
|
||||
/* FLO Cloud operations to send/request application data*/
|
||||
'use strict';
|
||||
const floCloudAPI = EXPORTS;
|
||||
@ -195,14 +195,24 @@
|
||||
floCloudAPI.init = function startCloudProcess(nodes) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// accept only plain-ish objects
|
||||
nodes = (nodes && typeof nodes === 'object' && !Array.isArray(nodes)) ? nodes : {};
|
||||
|
||||
supernodes = nodes;
|
||||
|
||||
// reset liveness bookkeeping for the new set
|
||||
if (_inactive && typeof _inactive.clear === 'function') _inactive.clear();
|
||||
|
||||
// (re)build the bucket with current IDs
|
||||
kBucket = new K_Bucket(DEFAULT.SNStorageID, Object.keys(supernodes));
|
||||
|
||||
resolve('Cloud init successful');
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Object.defineProperty(floCloudAPI, 'kBucket', {
|
||||
get: () => kBucket
|
||||
@ -227,24 +237,27 @@
|
||||
|
||||
function ws_activeConnect(snID, reverse = false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (_inactive.size === kBucket.list.length)
|
||||
// Safe guard: uninitialized kBucket, empty list, or all inactive
|
||||
if (!kBucket || !kBucket.list || !kBucket.list.length || _inactive.size === kBucket.list.length)
|
||||
return reject('Cloud offline');
|
||||
if (!(snID in supernodes))
|
||||
snID = kBucket.closestNode(proxyID(snID));
|
||||
|
||||
if (!(snID in supernodes)) {
|
||||
var closest = kBucket.closestNode(proxyID(snID));
|
||||
if (!closest) return reject('Cloud offline'); // no candidate to try
|
||||
snID = closest;
|
||||
}
|
||||
|
||||
ws_connect(snID)
|
||||
.then(node => resolve(node))
|
||||
.catch(error => {
|
||||
if (reverse)
|
||||
var nxtNode = kBucket.prevNode(snID);
|
||||
else
|
||||
var nxtNode = kBucket.nextNode(snID);
|
||||
ws_activeConnect(nxtNode, reverse)
|
||||
.then(node => resolve(node))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
})
|
||||
var nxtNode = reverse ? kBucket.prevNode(snID) : kBucket.nextNode(snID);
|
||||
if (!nxtNode || nxtNode === snID) return reject('Cloud offline'); // nothing else to try
|
||||
ws_activeConnect(nxtNode, reverse).then(resolve).catch(reject);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function fetch_API(snID, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (_inactive.has(snID))
|
||||
@ -265,25 +278,28 @@
|
||||
|
||||
function fetch_ActiveAPI(snID, data, reverse = false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (_inactive.size === kBucket.list.length)
|
||||
// Safe guard: uninitialized kBucket, empty list, or all inactive
|
||||
if (!kBucket || !kBucket.list || !kBucket.list.length || _inactive.size === kBucket.list.length)
|
||||
return reject('Cloud offline');
|
||||
if (!(snID in supernodes))
|
||||
snID = kBucket.closestNode(proxyID(snID));
|
||||
|
||||
if (!(snID in supernodes)) {
|
||||
var closest = kBucket.closestNode(proxyID(snID));
|
||||
if (!closest) return reject('Cloud offline'); // no candidate available
|
||||
snID = closest;
|
||||
}
|
||||
|
||||
fetch_API(snID, data)
|
||||
.then(result => resolve(result))
|
||||
.then(resolve)
|
||||
.catch(error => {
|
||||
_inactive.add(snID)
|
||||
if (reverse)
|
||||
var nxtNode = kBucket.prevNode(snID);
|
||||
else
|
||||
var nxtNode = kBucket.nextNode(snID);
|
||||
fetch_ActiveAPI(nxtNode, data, reverse)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
})
|
||||
})
|
||||
_inactive.add(snID);
|
||||
var nxtNode = reverse ? kBucket.prevNode(snID) : kBucket.nextNode(snID);
|
||||
if (!nxtNode || nxtNode === snID) return reject('Cloud offline'); // nothing else to try
|
||||
fetch_ActiveAPI(nxtNode, data, reverse).then(resolve).catch(reject);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function singleRequest(floID, data_obj, method = "POST") {
|
||||
return new Promise((resolve, reject) => {
|
||||
let data;
|
||||
@ -374,13 +390,31 @@
|
||||
|
||||
const util = floCloudAPI.util = {};
|
||||
|
||||
//Updating encoding/Decoding to modern standards
|
||||
const encodeMessage = util.encodeMessage = function (message) {
|
||||
return btoa(unescape(encodeURIComponent(JSON.stringify(message))))
|
||||
}
|
||||
const bytes = new TextEncoder().encode(JSON.stringify(message));
|
||||
let bin = "";
|
||||
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
||||
return btoa(bin);
|
||||
};
|
||||
|
||||
const decodeMessage = util.decodeMessage = function (message) {
|
||||
return JSON.parse(decodeURIComponent(escape(atob(message))))
|
||||
}
|
||||
try {
|
||||
// try modern decode first
|
||||
const bin = atob(message);
|
||||
const bytes = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
||||
return JSON.parse(new TextDecoder().decode(bytes));
|
||||
} catch (e1) {
|
||||
try {
|
||||
// fallback to legacy decode
|
||||
return JSON.parse(decodeURIComponent(escape(atob(message))));
|
||||
} catch (e2) {
|
||||
// final fallback: return raw string to avoid hard crash
|
||||
return message;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const filterKey = util.filterKey = function (type, options = {}) {
|
||||
return type + (options.comment ? ':' + options.comment : '') +
|
||||
@ -468,21 +502,59 @@
|
||||
|
||||
function storeGeneral(fk, dataSet) {
|
||||
try {
|
||||
console.log(dataSet)
|
||||
if (typeof generalData[fk] !== "object")
|
||||
generalData[fk] = {}
|
||||
for (let vc in dataSet) {
|
||||
generalData[fk][vc] = dataSet[vc];
|
||||
if (dataSet[vc].log_time > lastVC[fk])
|
||||
lastVC[fk] = dataSet[vc].log_time;
|
||||
if (!dataSet || typeof dataSet !== "object") return;
|
||||
|
||||
// Ensure containers exist
|
||||
if (typeof generalData[fk] !== "object" || generalData[fk] === null)
|
||||
generalData[fk] = {};
|
||||
if (typeof lastVC[fk] !== "number")
|
||||
lastVC[fk] = 0;
|
||||
|
||||
// Merge data and track latest log_time
|
||||
let updated = false;
|
||||
let newLast = lastVC[fk];
|
||||
|
||||
for (const vc in dataSet) {
|
||||
const rec = dataSet[vc];
|
||||
// Skip bad records
|
||||
if (!rec || typeof rec !== "object") continue;
|
||||
|
||||
// Assign only if changed reference (cheap check)
|
||||
if (generalData[fk][vc] !== rec) {
|
||||
generalData[fk][vc] = rec;
|
||||
updated = true;
|
||||
}
|
||||
if (typeof rec.log_time === "number" && rec.log_time > newLast) {
|
||||
newLast = rec.log_time;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
compactIDB.writeData("lastVC", lastVC[fk], fk)
|
||||
compactIDB.writeData("generalData", generalData[fk], fk)
|
||||
|
||||
if (!updated) return; // nothing new, avoid IDB writes
|
||||
|
||||
lastVC[fk] = newLast;
|
||||
|
||||
// --- Debounce writes per fk to avoid IDB thrash ---
|
||||
storeGeneral._pending = storeGeneral._pending || Object.create(null);
|
||||
const pend = storeGeneral._pending;
|
||||
|
||||
clearTimeout(pend[fk]);
|
||||
pend[fk] = setTimeout(() => {
|
||||
try {
|
||||
// Fire-and-forget; callers don’t wait on these
|
||||
compactIDB.writeData("lastVC", lastVC[fk], fk);
|
||||
compactIDB.writeData("generalData", generalData[fk], fk);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}, 50);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function objectifier(data) {
|
||||
if (!Array.isArray(data))
|
||||
data = [data];
|
||||
@ -551,7 +623,7 @@
|
||||
}
|
||||
|
||||
//request any data from supernode cloud
|
||||
const requestApplicationData = floCloudAPI.requestApplicationData = function (type, options = {}) {
|
||||
const _requestApplicationData = function (type, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var request = {
|
||||
receiverID: options.receiverID || DEFAULT.adminID,
|
||||
@ -582,6 +654,17 @@
|
||||
})
|
||||
}
|
||||
|
||||
floCloudAPI.requestApplicationData = function (type, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let single_request_mode = !(options.callback instanceof Function);
|
||||
_requestApplicationData(type, options).then(data => {
|
||||
if (single_request_mode)
|
||||
resolve(objectifier(data))
|
||||
else resolve(data);
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
/*(NEEDS UPDATE)
|
||||
//delete data from supernode cloud (received only)
|
||||
floCloudAPI.deleteApplicationData = function(vectorClocks, options = {}) {
|
||||
@ -615,7 +698,7 @@
|
||||
//request the data from cloud for resigning
|
||||
let req_options = Object.assign({}, options);
|
||||
req_options.atVectorClock = vectorClock;
|
||||
requestApplicationData(undefined, req_options).then(result => {
|
||||
_requestApplicationData(undefined, req_options).then(result => {
|
||||
if (!result.length)
|
||||
return reject("Data not found");
|
||||
let data = result[0];
|
||||
@ -709,11 +792,11 @@
|
||||
storeGeneral(fk, d);
|
||||
options.callback(d, e)
|
||||
}
|
||||
requestApplicationData(type, new_options)
|
||||
_requestApplicationData(type, new_options)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
} else {
|
||||
requestApplicationData(type, options).then(dataSet => {
|
||||
_requestApplicationData(type, options).then(dataSet => {
|
||||
storeGeneral(fk, objectifier(dataSet))
|
||||
resolve(dataSet)
|
||||
}).catch(error => reject(error))
|
||||
@ -738,7 +821,7 @@
|
||||
}
|
||||
delete options.callback;
|
||||
}
|
||||
requestApplicationData(objectName, options).then(dataSet => {
|
||||
_requestApplicationData(objectName, options).then(dataSet => {
|
||||
updateObject(objectName, objectifier(dataSet));
|
||||
delete options.comment;
|
||||
options.lowerVectorClock = lastVC[objectName] + 1;
|
||||
@ -746,11 +829,11 @@
|
||||
if (callback) {
|
||||
let new_options = Object.create(options);
|
||||
new_options.callback = callback;
|
||||
requestApplicationData(objectName, new_options)
|
||||
_requestApplicationData(objectName, new_options)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
} else {
|
||||
requestApplicationData(objectName, options).then(dataSet => {
|
||||
_requestApplicationData(objectName, options).then(dataSet => {
|
||||
updateObject(objectName, objectifier(dataSet))
|
||||
resolve(appObjects[objectName])
|
||||
}).catch(error => reject(error))
|
||||
@ -825,7 +908,7 @@
|
||||
floCloudAPI.downloadFile = function (vectorClock, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
options.atVectorClock = vectorClock;
|
||||
requestApplicationData(options.type, options).then(result => {
|
||||
_requestApplicationData(options.type, options).then(result => {
|
||||
if (!result.length)
|
||||
return reject("File not found");
|
||||
result = result[0];
|
||||
@ -1103,4 +1186,4 @@
|
||||
})();
|
||||
|
||||
|
||||
})('object' === typeof module ? module.exports : window.floCloudAPI = {});
|
||||
})('object' === typeof module ? module.exports : window.floCloudAPI = {});
|
||||
|
||||
115
scripts/ribc.js
115
scripts/ribc.js
@ -149,6 +149,41 @@
|
||||
return internRequests;
|
||||
}
|
||||
|
||||
// Resolve per-intern task status by latest timestamp (assigned/completed/failed/reopened)
|
||||
Ribc.getLatestTaskStatus = function (floID, taskId) {
|
||||
const rec = Ribc.getInternRecord(floID) || {};
|
||||
const events = [];
|
||||
|
||||
const assignedOn = rec.assignedTasks?.[taskId]?.assignedOn;
|
||||
if (assignedOn) events.push({ s: 'active', t: assignedOn });
|
||||
|
||||
const compDate = rec.completedTasks?.[taskId]?.completionDate;
|
||||
if (compDate) events.push({ s: 'completed', t: compDate });
|
||||
|
||||
const failDate = rec.failedTasks?.[taskId]?.failedDate;
|
||||
if (failDate) events.push({ s: 'failed', t: failDate });
|
||||
|
||||
const reopenDate = rec.reopenedTasks?.[taskId]?.reopenedDate;
|
||||
if (reopenDate) events.push({ s: 'active', t: reopenDate });
|
||||
|
||||
if (!events.length) return 'active';
|
||||
events.sort((a, b) => a.t - b.t);
|
||||
return events[events.length - 1].s; // latest wins
|
||||
}
|
||||
// Aggregate latest status across assigned interns
|
||||
Ribc.getAggregateTaskStatus = function (taskId) {
|
||||
const internIds = Ribc.getAssignedInterns(taskId) || [];
|
||||
if (!internIds.length) return Ribc.getTaskStatus(taskId) || 'active';
|
||||
|
||||
const states = internIds.map(id => Ribc.getLatestTaskStatus(id, taskId)); // 'active'|'completed'|'failed'
|
||||
|
||||
if (states.length && states.every(s => s === 'completed')) return 'completed';
|
||||
if (states.some(s => s === 'active')) return 'active';
|
||||
if (states.every(s => s === 'failed')) return 'failed';
|
||||
return 'mixed';
|
||||
};
|
||||
|
||||
|
||||
Admin.processInternRequest = function (vectorClock, accept = true) {
|
||||
let request = floGlobals.generalDataset("InternRequests")[vectorClock];
|
||||
if (!request)
|
||||
@ -216,6 +251,84 @@
|
||||
_.internRating[floID] = parseInt(totalScore / (completedTasks + failedTasks) || 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// (existing) Append-only "reopened" stamp
|
||||
Admin.addReopenedTask = function (floID, taskKey, details = {}) {
|
||||
if (!(floID in _.internList)) return false;
|
||||
Admin.initInternRecord(floID);
|
||||
|
||||
if (!_.internRecord[floID].reopenedTasks)
|
||||
_.internRecord[floID].reopenedTasks = {};
|
||||
|
||||
const reopenedDate = details.reopenedDate || Date.now();
|
||||
_.internRecord[floID].reopenedTasks[taskKey] = { reopenedDate };
|
||||
return true;
|
||||
};
|
||||
|
||||
// (new) Remove a completion entry when a task is made incomplete
|
||||
Admin.removeCompletedTask = function (floID, taskKey) {
|
||||
if (!(floID in _.internList)) return false;
|
||||
Admin.initInternRecord(floID);
|
||||
const rec = _.internRecord[floID];
|
||||
if (rec?.completedTasks && taskKey in rec.completedTasks) {
|
||||
delete rec.completedTasks[taskKey];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// (existing) Recompute rating, ignoring completions superseded by later reopen
|
||||
Admin.recomputeRating = function (floID) {
|
||||
const rec = _.internRecord[floID];
|
||||
if (!rec) return;
|
||||
|
||||
let totalScore = 0;
|
||||
let denom = 0;
|
||||
|
||||
for (const key in (rec.completedTasks || {})) {
|
||||
const comp = rec.completedTasks[key];
|
||||
const compDate = comp.completionDate || 0;
|
||||
const reopenedDate = rec.reopenedTasks?.[key]?.reopenedDate || 0;
|
||||
if (reopenedDate > compDate) continue; // reopened later ⇒ ignore this completion
|
||||
totalScore += Number(comp.points) || 0;
|
||||
denom += 1;
|
||||
}
|
||||
|
||||
// keep failed tasks in denominator (your current policy)
|
||||
denom += Object.keys(rec.failedTasks || {}).length;
|
||||
|
||||
_.internRating[floID] = Math.floor(denom ? (totalScore / denom) : 1) || 1;
|
||||
};
|
||||
|
||||
// (optional) One-shot reconciliation if you want extra safety
|
||||
Admin.syncInternRecordWithTaskStatus = function (taskId) {
|
||||
const getAssigned = RIBC.getAssignedInterns || RIBC.admin?.getAssignedInterns;
|
||||
const assignedInterns = (typeof getAssigned === 'function' ? getAssigned(taskId) : []) || [];
|
||||
const status = RIBC.getTaskStatus?.(taskId) || RIBC.getAggregateTaskStatus?.(taskId);
|
||||
if (!status) return;
|
||||
|
||||
if (status === 'completed') {
|
||||
const completionDate = Date.now();
|
||||
assignedInterns.forEach(internId => {
|
||||
const rec = _.internRecord?.[internId];
|
||||
const has = !!(rec && rec.completedTasks && rec.completedTasks[taskId]);
|
||||
if (!has) RIBC.admin.addCompletedTask(internId, taskId, 0, { completionDate });
|
||||
RIBC.admin.recomputeRating?.(internId);
|
||||
});
|
||||
} else if (status === 'incomplete') {
|
||||
const reopenedDate = Date.now();
|
||||
assignedInterns.forEach(internId => {
|
||||
RIBC.admin.addReopenedTask(internId, taskId, { reopenedDate });
|
||||
RIBC.admin.removeCompletedTask?.(internId, taskId);
|
||||
RIBC.admin.recomputeRating?.(internId);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
Admin.addFailedTask = function (floID, taskKey, details = {}) {
|
||||
if (!(floID in _.internList))
|
||||
return false;
|
||||
@ -590,4 +703,4 @@
|
||||
return returnData;
|
||||
}
|
||||
|
||||
})();
|
||||
})();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user