Massive refactoring to search/display code on multiple boards, and many other features. More changes coming soon.

This commit is contained in:
crowetic 2025-01-27 21:03:13 -08:00
parent 10f8230619
commit 5a35fb0d07
6 changed files with 614 additions and 408 deletions

View File

@ -59,6 +59,13 @@ const loadAddRemoveAdminPage = async () => {
<div id="existing-proposals-section" class="proposals-section" style="margin-top: 3em; display: flex; flex-direction: column; justify-content: center; align-items: center;"> <div id="existing-proposals-section" class="proposals-section" style="margin-top: 3em; display: flex; flex-direction: column; justify-content: center; align-items: center;">
<h3 style="color: #ddd;">Existing Promotion/Demotion Proposals</h3> <h3 style="color: #ddd;">Existing Promotion/Demotion Proposals</h3>
<button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Proposal Cards</button> <button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Proposal Cards</button>
<select id="time-range-select" style="margin-left: 10px; padding: 5px;">
<option value="0">Show All</option>
<option value="1">Last 1 day</option>
<option value="7">Last 7 days</option>
<option value="30" selected>Last 30 days</option>
<option value="90">Last 90 days</option>
</select>
</div> </div>
<div id="cards-container" class="cards-container" style="margin-top: 1rem""> <div id="cards-container" class="cards-container" style="margin-top: 1rem"">
<!-- We'll fill this with existing proposal cards --> <!-- We'll fill this with existing proposal cards -->
@ -80,8 +87,8 @@ const loadAddRemoveAdminPage = async () => {
// proposeButton.style.display === 'flex' ? 'none' : 'flex' // proposeButton.style.display === 'flex' ? 'none' : 'flex'
} catch (error) { } catch (error) {
console.error("Error checking for existing card:", error) console.error("Error opening propose form", error)
alert("Failed to check for existing card. Please try again.") alert("Failed to open proposal form. Please try again.")
} }
}) })
@ -126,79 +133,79 @@ const toggleProposeButton = () => {
proposeButton.style.display === 'flex' ? 'none' : 'flex' proposeButton.style.display === 'flex' ? 'none' : 'flex'
} }
let addAdminTxs
let remAdminTxs
const fetchAllARTxData = async () => { const fetchAllARTxData = async () => {
const addAdmTx = "ADD_GROUP_ADMIN" const addAdmTx = "ADD_GROUP_ADMIN"
const remAdmTx = "REMOVE_GROUP_ADMIN" const remAdmTx = "REMOVE_GROUP_ADMIN"
const filterAddTransactions = (rawTransactions) => {
// Group transactions by member
const memberTxMap = rawTransactions.reduce((map, tx) => {
if (!map[tx.member]) {
map[tx.member] = []
}
map[tx.member].push(tx)
return map
}, {})
// Filter out members with both pending and non-pending transactions
return Object.values(memberTxMap)
.filter(txs => txs.every(tx => tx.approvalStatus !== 'PENDING'))
.flat()
// .filter((txs) => !(txs.some(tx => tx.approvalStatus === 'PENDING') &&
// txs.some(tx => tx.approvalStatus !== 'PENDING')))
// .flat()
}
const filterRemoveTransactions = (rawTransactions) => {
// Group transactions by member
const adminTxMap = rawTransactions.reduce((map, tx) => {
if (!map[tx.admin]) {
map[tx.admin] = []
}
map[tx.admin].push(tx)
return map
}, {})
// Filter out members with both pending and non-pending transactions
return Object.values(adminTxMap)
.filter((txs) => !(txs.some(tx => tx.approvalStatus === 'PENDING') &&
txs.some(tx => tx.approvalStatus !== 'PENDING')))
.flat()
}
// Fetch ban transactions
const allAddTxs = await searchTransactions({ const allAddTxs = await searchTransactions({
txTypes: [addAdmTx], txTypes: [addAdmTx],
confirmationStatus: 'CONFIRMED', confirmationStatus: 'CONFIRMED',
limit: 0, limit: 0,
reverse: true, reverse: true,
offset: 0, offset: 0,
startBlock: 1990000, startBlock: 1990000,
blockLimit: 0, blockLimit: 0,
txGroupId: 694, txGroupId: 694,
}) })
// Filter out 'PENDING'
addAdminTxs = filterAddTransactions(allAddTxs) const allRemTxs = await searchTransactions({
console.warn('addAdminTxData (no PENDING nor past+PENDING):', addAdminTxs) txTypes: [remAdmTx],
confirmationStatus: 'CONFIRMED',
limit: 0,
reverse: true,
offset: 0,
startBlock: 1990000,
blockLimit: 0,
txGroupId: 694,
})
const { finalAddTxs, pendingAddTxs } = partitionAddTransactions(allAddTxs)
const { finalRemTxs, pendingRemTxs } = partitionRemoveTransactions(allRemTxs)
// We are going to keep all transactions in order to filter more accurately for display purposes.
console.log('Final addAdminTxs:', finalAddTxs);
console.log('Pending addAdminTxs:', pendingAddTxs);
console.log('Final remAdminTxs:', finalRemTxs);
console.log('Pending remAdminTxs:', pendingRemTxs);
return {
finalAddTxs,
pendingAddTxs,
finalRemTxs,
pendingRemTxs,
}
}
function partitionAddTransactions(rawTransactions) {
const finalAddTxs = []
const pendingAddTxs = []
for (const tx of rawTransactions) {
if (tx.approvalStatus === 'PENDING') {
pendingAddTxs.push(tx)
} else {
finalAddTxs.push(tx)
}
}
return { finalAddTxs, pendingAddTxs };
}
function partitionRemoveTransactions(rawTransactions) {
const finalRemTxs = []
const pendingRemTxs = []
for (const tx of rawTransactions) {
if (tx.approvalStatus === 'PENDING') {
pendingRemTxs.push(tx)
} else {
finalRemTxs.push(tx)
}
}
return { finalRemTxs, pendingRemTxs }
}
// Fetch kick transactions
const allRemTxs = await searchTransactions({
txTypes: [remAdmTx],
confirmationStatus: 'CONFIRMED',
limit: 0,
reverse: true,
offset: 0,
startBlock: 1990000,
blockLimit: 0,
txGroupId: 694,
})
// Filter out 'PENDING'
remAdminTxs = filterRemoveTransactions(allRemTxs)
console.warn('remAdminTxData (no PENDING nor past+PENDING):', remAdminTxs)
}
const displayExistingMinterAdmins = async () => { const displayExistingMinterAdmins = async () => {
const adminListContainer = document.getElementById("admin-list-container") const adminListContainer = document.getElementById("admin-list-container")
@ -309,9 +316,10 @@ const handleProposeDemotion = async (adminName, adminAddress) => {
// Notify the user to fill out the rest // Notify the user to fill out the rest
alert(`Admin "${adminName}" has been selected for demotion. Please fill out the rest of the form.`) alert(`Admin "${adminName}" has been selected for demotion. Please fill out the rest of the form.`)
} }
const fetchExistingARCard = async (cardIdentifierPrefix, minterName) => { const fetchExistingARCard = async (cardIdentifierPrefix, minterName) => {
try { try {
const response = await searchSimple( const response = await searchSimple(
'BLOG_POST', 'BLOG_POST',
@ -381,7 +389,7 @@ const handleProposeDemotion = async (adminName, adminAddress) => {
console.error("Error fetching existing AR card:", error) console.error("Error fetching existing AR card:", error)
return null return null
} }
} }
const publishARCard = async (cardIdentifierPrefix) => { const publishARCard = async (cardIdentifierPrefix) => {
@ -726,8 +734,8 @@ const fallbackMinterCheck = async (minterName, minterGroupMembers, minterAdmins)
} }
const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => { const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, bgColor, cardPublisherAddress, illegalDuplicate) => {
const { minterName, header, content, links, creator, timestamp, poll, promotionCard } = cardData const { minterName, minterAddress='priorToAddition', header, content, links, creator, timestamp, poll, promotionCard } = cardData
const formattedDate = new Date(timestamp).toLocaleString() const formattedDate = new Date(timestamp).toLocaleString()
const minterAvatar = await getMinterAvatar(minterName) const minterAvatar = await getMinterAvatar(minterName)
const creatorAvatar = await getMinterAvatar(creator) const creatorAvatar = await getMinterAvatar(creator)
@ -744,7 +752,7 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
// showPromotionCard = await fallbackMinterCheck(minterName, minterGroupMembers, minterAdmins) // showPromotionCard = await fallbackMinterCheck(minterName, minterGroupMembers, minterAdmins)
if (typeof promotionCard === 'boolean') { if (typeof promotionCard === 'boolean') {
showPromotionCard = promotionCard; showPromotionCard = promotionCard
} else if (typeof promotionCard === 'string') { } else if (typeof promotionCard === 'string') {
// Could be "true" or "false" or something else // Could be "true" or "false" or something else
const lower = promotionCard.trim().toLowerCase() const lower = promotionCard.trim().toLowerCase()
@ -790,41 +798,63 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
let altText = '' let altText = ''
const verifiedName = await validateMinterName(minterName) const verifiedName = await validateMinterName(minterName)
if (verifiedName) { if (verifiedName && !illegalDuplicate) {
const accountInfo = await getNameInfo(verifiedName) const accountInfo = await getNameInfo(verifiedName)
const accountAddress = accountInfo.owner const accountAddress = accountInfo.owner
const minterGroupAddresses = minterGroupMembers.map(m => m.member)
const adminAddresses = minterAdmins.map(m => m.member)
const existingAdmin = adminAddresses.includes(accountAddress)
const existingMinter = minterGroupAddresses.includes(accountAddress)
console.log(`name is validated, utilizing for removal features...${verifiedName}`) console.log(`name is validated, utilizing for removal features...${verifiedName}`)
const actionsHtmlCheck = await checkAndDisplayActions(adminYes, verifiedName, cardIdentifier) const actionsHtmlCheck = await checkAndDisplayActions(adminYes, verifiedName, cardIdentifier)
actionsHtml = actionsHtmlCheck actionsHtml = actionsHtmlCheck
if (!addAdminTxs || !remAdminTxs) { const { finalAddTxs, pendingAddTxs, finalRemTxs, pendingRemTxs } = await fetchAllARTxData()
await fetchAllARTxData()
}
if (addAdminTxs.some((addTx) => addTx.groupId === 694 && addTx.member === accountAddress)){ const confirmedAdd = finalAddTxs.some(
console.warn(`account was already adminified(PROMOTED), displaying as such...`) (tx) => tx.groupId === 694 && tx.member === accountAddress
)
const userPendingAdd = pendingAddTxs.some(
(tx) => tx.groupId === 694 && tx.member === accountAddress
)
const confirmedRemove = finalRemTxs.some(
(tx) => tx.groupId === 694 && tx.admin === accountAddress
)
const userPendingRemove = pendingRemTxs.some(
(tx) => tx.groupId === 694 && tx.admin === accountAddress
)
// If user is definitely admin (finalAdd) and not pending removal
if (confirmedAdd && !userPendingRemove && existingAdmin) {
console.warn(`account was already admin, final. no add/remove pending.`);
cardColorCode = 'rgb(3, 11, 24)' cardColorCode = 'rgb(3, 11, 24)'
altText = `<h4 style="color:rgb(2, 94, 106); margin-bottom: 0.5em;">PROMOTED to ADMIN</h4>` altText = `<h4 style="color:rgb(2, 94, 106); margin-bottom: 0.5em;">PROMOTED to ADMIN</h4>`;
actionsHtml = '' actionsHtml = ''
// =============================================================== if 'showPromotedDemoted' is wanted to be added. }
// if (!showPromotedDemoted){
// console.warn(`promoted/demoted show checkbox is unchecked, and card is promoted, not displaying...`) if (confirmedAdd && userPendingRemove && existingAdmin) {
// return '' console.warn(`user is a previously approved an admin, but now has pending removals. Keeping html`)
// }
} }
if (remAdminTxs.some((remTx) => remTx.groupId === 694 && remTx.admin === accountAddress)){ // If user has a final "remove" and no pending additions or removals
console.warn(`account was already UNadminified(DEMOTED), displaying as such...`) if (confirmedRemove && !userPendingAdd && existingMinter) {
console.warn(`account was demoted, final. no add pending, existingMinter.`);
cardColorCode = 'rgb(29, 4, 6)' cardColorCode = 'rgb(29, 4, 6)'
altText = `<h4 style="color:rgb(73, 24, 24); margin-bottom: 0.5em;">DEMOTED from ADMIN</h4>` altText = `<h4 style="color:rgb(73, 24, 24); margin-bottom: 0.5em;">DEMOTED from ADMIN</h4>`
actionsHtml = '' actionsHtml = ''
// if (!showPromotedDemoted) {
// console.warn(`promoted/demoted show checkbox is unchecked, card is demoted, not displaying...`)
// return ''
// }
} }
// If user has both final remove and pending add, do something else
if (confirmedRemove && userPendingAdd && existingMinter) {
console.warn(`account was previously demoted, but also a pending re-add, allowing actions to show...`)
// Possibly show "DEMOTED but re-add in progress" or something
}
} else if ( verifiedName && illegalDuplicate) {
console.warn(`illegalDuplicate detected (this card was somehow allowed to be published twice, keeping newest as active to prevent issues with old cards and updates, but displaying without actions...)`)
cardColorCode = 'rgb(82, 81, 81)'
altText = `<h4 style="color:rgb(21, 30, 39); margin-bottom: 0.5em;">DUPLICATE (diplayed for data only)</h4>`
actionsHtml = ''
} else { } else {
console.warn(`name could not be validated, not setting actionsHtml`) console.warn(`name could not be validated, not setting actionsHtml`)
actionsHtml = '' actionsHtml = ''

View File

@ -83,6 +83,13 @@ const loadAdminBoardPage = async () => {
<option value="least-votes">Least Votes</option> <option value="least-votes">Least Votes</option>
<option value="most-votes">Most Votes</option> <option value="most-votes">Most Votes</option>
</select> </select>
<select id="time-range-select" style="margin-left: 10px; padding: 5px;">
<option value="0">Show All</option>
<option value="1">Last 1 day</option>
<option value="7">Last 7 days</option>
<option value="30" selected>Last 30 days</option>
<option value="90">Last 90 days</option>
</select>
<div class="show-card-checkbox" style="margin-top: 1em;"> <div class="show-card-checkbox" style="margin-top: 1em;">
<input type="checkbox" id="admin-show-hidden-checkbox" name="adminHidden" /> <input type="checkbox" id="admin-show-hidden-checkbox" name="adminHidden" />
<label for="admin-show-hidden-checkbox">Show User-Hidden Cards?</label> <label for="admin-show-hidden-checkbox">Show User-Hidden Cards?</label>
@ -327,8 +334,20 @@ const fetchAllEncryptedCards = async (isRefresh = false) => {
const encryptedCardsContainer = document.getElementById("encrypted-cards-container") const encryptedCardsContainer = document.getElementById("encrypted-cards-container")
encryptedCardsContainer.innerHTML = "<p>Loading cards...</p>" encryptedCardsContainer.innerHTML = "<p>Loading cards...</p>"
let afterTime = null
const timeRangeSelect = document.getElementById("time-range-select")
if (timeRangeSelect) {
const days = parseInt(timeRangeSelect.value, 10)
if (days > 0) {
const now = Date.now()
const dayMs = 24 * 60 * 60 * 1000
afterTime = now - days * dayMs // e.g. last X days
console.log(`afterTime for last ${days} days = ${new Date(afterTime).toLocaleString()}`)
}
}
try { try {
const response = await searchSimple('MAIL_PRIVATE', `${encryptedCardIdentifierPrefix}`, '', 0) const response = await searchSimple('MAIL_PRIVATE', `${encryptedCardIdentifierPrefix}`, '', 0, 0, '', false, true, afterTime)
if (!response || !Array.isArray(response) || response.length === 0) { if (!response || !Array.isArray(response) || response.length === 0) {
encryptedCardsContainer.innerHTML = "<p>No cards found.</p>" encryptedCardsContainer.innerHTML = "<p>No cards found.</p>"
@ -379,12 +398,13 @@ const fetchAllEncryptedCards = async (isRefresh = false) => {
const latestCardsMap = new Map() const latestCardsMap = new Map()
validCardsWithData.forEach(({ card, decryptedCardData }) => { validCardsWithData.forEach(({ card, decryptedCardData }) => {
const timestamp = card.updated || card.created || 0 const timestamp = card.created || 0
const existingCard = latestCardsMap.get(card.identifier) const existingCard = latestCardsMap.get(card.identifier)
if (!existingCard || timestamp > (existingCard.card.updated || existingCard.card.created || 0)) { if (!existingCard || timestamp < (existingCard.card.updated || existingCard.card.created || 0)) {
latestCardsMap.set(card.identifier, { card, decryptedCardData }) latestCardsMap.set(card.identifier, { card, decryptedCardData })
} }
}) })
const uniqueValidCards = Array.from(latestCardsMap.values()) const uniqueValidCards = Array.from(latestCardsMap.values())
@ -396,13 +416,11 @@ const fetchAllEncryptedCards = async (isRefresh = false) => {
const obtainedMinterName = decryptedCardData.minterName const obtainedMinterName = decryptedCardData.minterName
// Only check for cards that are NOT topic-based cards // Only check for cards that are NOT topic-based cards
if ((!decryptedCardData.isTopic) || decryptedCardData.isTopic === 'false') { if ((!decryptedCardData.isTopic) || decryptedCardData.isTopic === 'false') {
const cardTimestamp = card.updated || card.created || 0
if (obtainedMinterName) { if (obtainedMinterName) {
const existingEntry = mostRecentCardsMap.get(obtainedMinterName) const existingEntry = mostRecentCardsMap.get(obtainedMinterName)
// Replace only if the current card is more recent if (!existingEntry) {
if (!existingEntry || cardTimestamp > (existingEntry.card.updated || existingEntry.card.created || 0)) {
mostRecentCardsMap.set(obtainedMinterName, { card, decryptedCardData }) mostRecentCardsMap.set(obtainedMinterName, { card, decryptedCardData })
} }
} }
@ -414,7 +432,7 @@ const fetchAllEncryptedCards = async (isRefresh = false) => {
}) })
// Convert the map into an array of final cards // Convert the map into an array of final cards
const finalCards = Array.from(mostRecentCardsMap.values()); const finalCards = Array.from(mostRecentCardsMap.values())
let selectedSort = 'newest' let selectedSort = 'newest'
const sortSelect = document.getElementById('sort-select') const sortSelect = document.getElementById('sort-select')
@ -1037,7 +1055,7 @@ const processQortalLinkForRendering = async (link) => {
return link return link
} }
const checkAndDisplayRemoveActions = async (adminYes, name, cardIdentifier) => { const checkAndDisplayRemoveActions = async (adminYes, name, cardIdentifier, nameIsActuallyAddress = false) => {
const latestBlockInfo = await getLatestBlockInfo() const latestBlockInfo = await getLatestBlockInfo()
const isBlockPassed = latestBlockInfo.height >= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT const isBlockPassed = latestBlockInfo.height >= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT
let minAdminCount let minAdminCount
@ -1052,10 +1070,15 @@ const checkAndDisplayRemoveActions = async (adminYes, name, cardIdentifier) => {
minAdminCount = Math.round(fortyPercent) minAdminCount = Math.round(fortyPercent)
console.warn(`this is another check to ensure minterAdmin group has more than 1 admin. IF so we will calculate the 40% needed for GROUP_APPROVAL, that number is: ${minAdminCount}`) console.warn(`this is another check to ensure minterAdmin group has more than 1 admin. IF so we will calculate the 40% needed for GROUP_APPROVAL, that number is: ${minAdminCount}`)
} }
if (isBlockPassed && userState.isMinterAdmin) { if (isBlockPassed && (userState.isMinterAdmin || userState.isAdmin)) {
console.warn(`feature trigger has passed, checking for approval requirements`) console.warn(`feature trigger has passed, checking for approval requirements`)
const addressInfo = await getNameInfo(name) let address
const address = addressInfo.owner if (!nameIsActuallyAddress){
const nameInfo = await getNameInfo(name)
address = nameInfo.owner
} else {
address = name
}
const kickApprovalHtml = await checkGroupApprovalAndCreateButton(address, cardIdentifier, "GROUP_KICK") const kickApprovalHtml = await checkGroupApprovalAndCreateButton(address, cardIdentifier, "GROUP_KICK")
const banApprovalHtml = await checkGroupApprovalAndCreateButton(address, cardIdentifier, "GROUP_BAN") const banApprovalHtml = await checkGroupApprovalAndCreateButton(address, cardIdentifier, "GROUP_BAN")
@ -1068,7 +1091,7 @@ const checkAndDisplayRemoveActions = async (adminYes, name, cardIdentifier) => {
} }
} }
if (adminYes >= minAdminCount && userState.isMinterAdmin) { if (adminYes >= minAdminCount && (userState.isMinterAdmin || userState.isAdmin)) {
const removeButtonHtml = createRemoveButtonHtml(name, cardIdentifier) const removeButtonHtml = createRemoveButtonHtml(name, cardIdentifier)
return removeButtonHtml return removeButtonHtml
} else{ } else{
@ -1098,6 +1121,8 @@ const createRemoveButtonHtml = (name, cardIdentifier) => {
const handleKickMinter = async (minterName) => { const handleKickMinter = async (minterName) => {
try { try {
isAddress = await getAddressInfo(minterName)
// Optional block check // Optional block check
let txGroupId = 0 let txGroupId = 0
// const { height: currentHeight } = await getLatestBlockInfo() // const { height: currentHeight } = await getLatestBlockInfo()
@ -1108,8 +1133,14 @@ const handleKickMinter = async (minterName) => {
} }
// Get the minter address from name info // Get the minter address from name info
const minterNameInfo = await getNameInfo(minterName) let minterAddress
const minterAddress = minterNameInfo?.owner if (!isAddress){
const minterNameInfo = await getNameInfo(minterName)
minterAddress = minterNameInfo?.owner
} else {
minterAddress = minterName
}
if (!minterAddress) { if (!minterAddress) {
alert(`No valid address found for minter name: ${minterName}, this should NOT have happened, please report to developers...`) alert(`No valid address found for minter name: ${minterName}, this should NOT have happened, please report to developers...`)
return return
@ -1150,6 +1181,7 @@ const handleKickMinter = async (minterName) => {
} }
const handleBanMinter = async (minterName) => { const handleBanMinter = async (minterName) => {
isAddress = await getAddressInfo(minterName)
try { try {
let txGroupId = 0 let txGroupId = 0
// const { height: currentHeight } = await getLatestBlockInfo() // const { height: currentHeight } = await getLatestBlockInfo()
@ -1161,9 +1193,13 @@ const handleBanMinter = async (minterName) => {
console.log(`featureTrigger block is passed, using txGroupId 694`) console.log(`featureTrigger block is passed, using txGroupId 694`)
txGroupId = 694 txGroupId = 694
} }
let minterAddress
const minterNameInfo = await getNameInfo(minterName) if (!isAddress) {
const minterAddress = minterNameInfo?.owner const minterNameInfo = await getNameInfo(minterName)
const minterAddress = minterNameInfo?.owner
} else {
minterAddress = minterName
}
if (!minterAddress) { if (!minterAddress) {
alert(`No valid address found for minter name: ${minterName}, this should NOT have happened, please report to developers...`) alert(`No valid address found for minter name: ${minterName}, this should NOT have happened, please report to developers...`)
@ -1236,7 +1272,7 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
const showKickedBanned = document.getElementById('admin-show-kicked-banned-checkbox')?.checked ?? false const showKickedBanned = document.getElementById('admin-show-kicked-banned-checkbox')?.checked ?? false
const showHiddenAdminCards = document.getElementById('admin-show-hidden-checkbox')?.checked ?? false const showHiddenAdminCards = document.getElementById('admin-show-hidden-checkbox')?.checked ?? false
const isUndefinedUser = (minterName === 'undefined') const isUndefinedUser = (minterName === 'undefined' || minterName === 'null')
const hasTopicMode = Object.prototype.hasOwnProperty.call(cardData, 'topicMode') const hasTopicMode = Object.prototype.hasOwnProperty.call(cardData, 'topicMode')
@ -1254,7 +1290,7 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
let cardColorCode = showTopic ? '#0e1b15' : '#151f28' let cardColorCode = showTopic ? '#0e1b15' : '#151f28'
const minterOrTopicHtml = ((showTopic) || (isUndefinedUser)) ? ` const minterOrTopicHtml = ((showTopic) || (isUndefinedUser)) ? `
<div class="support-header"><h5> REGARDING (Topic): </h5></div> <div class="support-header"><h5> REGARDING (Topic / Address): </h5></div>
<h3>${minterName}` : <h3>${minterName}` :
` `
<div class="support-header"><h5> REGARDING (Name): </h5></div> <div class="support-header"><h5> REGARDING (Name): </h5></div>
@ -1274,16 +1310,23 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
let adjustmentText = '' let adjustmentText = ''
const verifiedName = await validateMinterName(minterName) const verifiedName = await validateMinterName(minterName)
let levelText = '</h3>' let levelText = '</h3>'
const addressVerification = await getAddressInfo(minterName)
const verifiedAddress = addressVerification.address
if (verifiedName) { if (verifiedName || verifiedAddress) {
const accountInfo = await getNameInfo(verifiedName) let accountInfo
const accountAddress = accountInfo.owner if (!verifiedAddress){
const addressInfo = await getAddressInfo(accountAddress) accountInfo = await getNameInfo(verifiedName)
}
const accountAddress = verifiedAddress ? addressVerification.address : accountInfo.owner
const addressInfo = verifiedAddress ? addressVerification : await getAddressInfo(accountAddress)
levelText = ` - Level ${addressInfo.level}</h3>` levelText = ` - Level ${addressInfo.level}</h3>`
console.log(`name is validated, utilizing for removal features...${verifiedName}`) console.log(`name is validated, utilizing for removal features...${verifiedName}`)
penaltyText = addressInfo.blocksMintedPenalty == 0 ? '' : '<p>(has Blocks Penalty)<p>' penaltyText = addressInfo.blocksMintedPenalty == 0 ? '' : '<p>(has Blocks Penalty)<p>'
adjustmentText = addressInfo.blocksMintedAdjustment == 0 ? '' : '<p>(has Blocks Adjustment)<p>' adjustmentText = addressInfo.blocksMintedAdjustment == 0 ? '' : '<p>(has Blocks Adjustment)<p>'
const removeActionsHtml = await checkAndDisplayRemoveActions(adminYes, verifiedName, cardIdentifier) const removeActionsHtml = verifiedAddress ? await checkAndDisplayRemoveActions(adminYes, verifiedAddress, cardIdentifier) : await checkAndDisplayRemoveActions(adminYes, verifiedName, cardIdentifier)
showRemoveHtml = removeActionsHtml showRemoveHtml = removeActionsHtml
if (userVote === 0) { if (userVote === 0) {
cardColorCode = "rgba(1, 65, 39, 0.41)"; // or any green you want cardColorCode = "rgba(1, 65, 39, 0.41)"; // or any green you want

View File

@ -28,10 +28,10 @@ async function loadMinterAdminToolsPage() {
<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; margin-right: 10px;"> <img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; margin-right: 10px;">
<span>${userState.accountName || 'Guest'}</span> <span>${userState.accountName || 'Guest'}</span>
</div> </div>
<div><h2>No Functionality Here Yet</h2></div> <div><h2>COMING SOON...</h2></div>
<div> <div>
<p>This page is still under development. Until the final Mintership proposal modifications are made, and the MINTER group is transferred to null, there is no need for this page's functionality. The page will be updated when the final modifications are made.</p> <p>This page will have functionality to assist the Minter Admins in performing their duties. It will display all pending transactions (of any kind they can approve/deny) along with that ability. It can also be utilized to obtain more in-depth information about existing accounts.</p>
<p> This page until then is simply a placeholder.</p> <p> The page will be getting a significant overhaul in the near(ish) future, as the MINTER group is now owned by null, and we are past the 'temporary state' we were in for much longer than planned.</p>
</div> </div>
</div> </div>

View File

@ -37,6 +37,13 @@ const loadMinterBoardPage = async () => {
<option value="least-votes">Least Votes</option> <option value="least-votes">Least Votes</option>
<option value="most-votes">Most Votes</option> <option value="most-votes">Most Votes</option>
</select> </select>
<select id="time-range-select" style="margin-left: 10px; padding: 5px;">
<option value="0">Show All</option>
<option value="1">Last 1 day</option>
<option value="7">Last 7 days</option>
<option value="30" selected>Last 30 days</option>
<option value="90">Last 90 days</option>
</select>
<div id="cards-container" class="cards-container" style="margin-top: 20px;"></div> <div id="cards-container" class="cards-container" style="margin-top: 20px;"></div>
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;"> <div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
<form id="publish-card-form"> <form id="publish-card-form">
@ -127,6 +134,11 @@ const loadMinterBoardPage = async () => {
await publishCard(minterCardIdentifierPrefix) await publishCard(minterCardIdentifierPrefix)
}) })
document.getElementById("time-range-select").addEventListener("change", async () => {
// Re-load the cards whenever user chooses a new sort option.
await loadCards(minterCardIdentifierPrefix)
})
document.getElementById("sort-select").addEventListener("change", async () => { document.getElementById("sort-select").addEventListener("change", async () => {
// Re-load the cards whenever user chooses a new sort option. // Re-load the cards whenever user chooses a new sort option.
await loadCards(minterCardIdentifierPrefix) await loadCards(minterCardIdentifierPrefix)
@ -149,11 +161,11 @@ const extractMinterCardsMinterName = async (cardIdentifier) => {
} }
try { try {
if (cardIdentifier.startsWith(minterCardIdentifierPrefix)){ if (cardIdentifier.startsWith(minterCardIdentifierPrefix)){
const searchSimpleResults = await searchSimple('BLOG_POST', `${cardIdentifier}`, '', 1) const searchSimpleResults = await searchSimple('BLOG_POST', `${cardIdentifier}`, '', 1, 0, '', false, true)
const minterName = await searchSimpleResults.name const minterName = await searchSimpleResults.name
return minterName return minterName
} else if (cardIdentifier.startsWith(addRemoveIdentifierPrefix)) { } else if (cardIdentifier.startsWith(addRemoveIdentifierPrefix)) {
const searchSimpleResults = await searchSimple('BLOG_POST', `${cardIdentifier}`, '', 1) const searchSimpleResults = await searchSimple('BLOG_POST', `${cardIdentifier}`, '', 1, 0, '', false, true)
const publisherName = searchSimpleResults.name const publisherName = searchSimpleResults.name
const cardDataResponse = await qortalRequest({ const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE", action: "FETCH_QDN_RESOURCE",
@ -161,103 +173,204 @@ const extractMinterCardsMinterName = async (cardIdentifier) => {
service: "BLOG_POST", service: "BLOG_POST",
identifier: cardIdentifier, identifier: cardIdentifier,
}) })
let nameInvalid = false
const minterName = cardDataResponse.minterName const minterName = cardDataResponse.minterName
return minterName if (minterName){
return minterName
} else {
nameInvalid = true
console.warn(`fuckery detected on identifier: ${cardIdentifier}, hello dipshit Mythril!, name invalid? Name doesn't match publisher? Returning invalid flag + publisherName...`)
return publisherName
}
} }
} catch (error) { } catch (error) {
throw error throw error
} }
} }
const processMinterCards = async (validMinterCards) => { const groupAndLabelByIdentifier = (allCards) => {
const latestCardsMap = new Map() // Group by identifier
const mapById = new Map()
// Deduplicate by identifier, keeping the most recent allCards.forEach(card => {
validMinterCards.forEach(card => { if (!mapById.has(card.identifier)) {
const timestamp = card.updated || card.created || 0 mapById.set(card.identifier, [])
const existingCard = latestCardsMap.get(card.identifier)
if (!existingCard || timestamp > (existingCard.updated || existingCard.created || 0)) {
latestCardsMap.set(card.identifier, card)
} }
mapById.get(card.identifier).push(card)
}) })
// Convert Map back to array // For each identifier's group, sort oldest->newest so the first is "master"
const uniqueValidCards = Array.from(latestCardsMap.values()) const output = []
for (const [identifier, group] of mapById.entries()) {
group.sort((a, b) => {
const aTime = a.created || 0
const bTime = b.created || 0
return aTime - bTime // oldest first
})
const minterGroupMembers = await fetchMinterGroupMembers() // Mark the first as master
const minterGroupAddresses = minterGroupMembers.map(m => m.member) group[0].isMaster = true
const minterNameMap = new Map() // The rest are updates
for (let i = 1; i < group.length; i++) {
group[i].isMaster = false
}
// For each card, extract minterName safely // push them all to output
for (const card of uniqueValidCards) { output.push(...group)
let minterName }
return output
}
// no semicolons, using arrow functions
const groupByIdentifierOldestFirst = (allCards) => {
// map of identifier => array of cards
const mapById = new Map()
allCards.forEach(card => {
if (!mapById.has(card.identifier)) {
mapById.set(card.identifier, [])
}
mapById.get(card.identifier).push(card)
})
// sort each group oldest->newest
for (const [identifier, group] of mapById.entries()) {
group.sort((a, b) => {
const aTime = a.created || 0
const bTime = b.created || 0
return aTime - bTime // oldest first
})
}
return mapById
}
// no semicolons, arrow functions
const buildMinterNameGroups = async (mapById) => {
// We'll build an array of objects: { minterName, cards }
// Then we can combine any that share the same minterName.
const nameGroups = []
for (let [identifier, group] of mapById.entries()) {
// group[0] is the oldest => "master" card
let masterCard = group[0]
// Filter out any cards that are not published by the 'masterPublisher'
const masterPublisherName = masterCard.name
// Remove any cards in this identifier group that have a different publisherName
const filteredGroup = group.filter(c => c.name === masterPublisherName)
// If filtering left zero cards, skip entire group
if (!filteredGroup.length) {
console.warn(`All cards removed for identifier=${identifier} (different publishers). Skipping.`)
continue
}
// Reassign group to the filtered version, then re-define masterCard
group = filteredGroup
masterCard = group[0] // oldest after filtering
// attempt to obtain minterName from the master card
let masterMinterName
try { try {
// If this throws, we catch below and skip masterMinterName = await extractMinterCardsMinterName(masterCard.identifier)
minterName = await extractMinterCardsMinterName(card.identifier) } catch (err) {
} catch (error) { console.warn(`Skipping entire group ${identifier}, no valid minterName from master`, err)
console.warn(
`Skipping card ${card.identifier} because extractMinterCardsMinterName failed:`,
error
)
continue // Skip this card and move on
}
console.log(`minterName`, minterName)
// Next, get minterNameInfo
const minterNameInfo = await getNameInfo(minterName)
if (!minterNameInfo) {
console.warn(`minterNameInfo is null for minter: ${minterName}, skipping card.`)
continue continue
} }
const minterAddress = minterNameInfo.owner // Store an object with the minterName we extracted, plus all cards in that group
// Validate the address nameGroups.push({
const addressValid = await getAddressInfo(minterAddress) minterName: masterMinterName,
if (!minterAddress || !addressValid) { cards: group // includes the master & updates
console.warn(`minterAddress invalid or missing for: ${minterName}, skipping card.`, minterAddress) })
continue }
}
// If this is a 'regular' minter card, skip if user is already a minter // Combine them: minterName => array of *all* cards from all matching groups
if (!card.identifier.includes('QM-AR-card')) { const combinedMap = new Map()
if (minterGroupAddresses.includes(minterAddress)) { for (const entry of nameGroups) {
console.log( const mName = entry.minterName
`existing minter found or fake name detected. Not including minter card: ${card.identifier}` if (!combinedMap.has(mName)) {
) combinedMap.set(mName, [])
continue }
combinedMap.get(mName).push(...entry.cards)
}
return combinedMap
}
const getNewestCardPerMinterName = (combinedMap) => {
// We'll produce an array of the newest card for each minterName, this will be utilized as the 'final filter' to display cards published/updated by unique minters.
const finalOutput = []
for (const [mName, cardArray] of combinedMap.entries()) {
// sort by updated or created, descending => newest first
cardArray.sort((a, b) => {
const aTime = a.updated || a.created || 0
const bTime = b.updated || b.created || 0
return bTime - aTime
})
// newest is [0]
finalOutput.push(cardArray[0])
}
// Then maybe globally sort them newest first
finalOutput.sort((a, b) => {
const aTime = a.updated || a.created || 0
const bTime = b.updated || b.created || 0
return bTime - aTime
})
return finalOutput
}
const processMinterBoardCards = async (allValidCards) => {
// group by identifier, sorted oldest->newest
const mapById = groupByIdentifierOldestFirst(allValidCards)
// build a map of minterName => all cards from those identifiers
const minterNameMap = await buildMinterNameGroups(mapById)
// from that map, keep only the single newest card per minterName
const newestCards = getNewestCardPerMinterName(minterNameMap)
// return final array of all newest cards
return newestCards
}
const processARBoardCards = async (allValidCards) => {
const mapById = groupByIdentifierOldestFirst(allValidCards)
// build a map of minterName => all cards from those identifiers
const mapByName = await buildMinterNameGroups(mapById)
// For each minterName group, we might want to sort them newest->oldest
const finalOutput = []
for (const [minterName, group] of mapByName.entries()) {
group.sort((a, b) => {
const aTime = a.updated || a.created || 0
const bTime = b.updated || b.created || 0
return bTime - aTime
})
// both resolution for the duplicate QuickMythril card, and handling of all future duplicates that may be published...
if (group[0].identifier === 'QM-AR-card-Xw3dxL') {
console.warn(`This is a bug that allowed a duplicate prior to the logic displaying them based on original publisher only... displaying in reverse order...`)
group[0].isDuplicate = true
for (let i = 1; i < group.length; i++) {
group[i].isDuplicate = false
}
}else {
group[0].isDuplicate = false
for (let i = 1; i < group.length; i++) {
group[i].isDuplicate = true
} }
} }
// push them all
// Keep only the most recent card for each minterName finalOutput.push(...group)
const existingCard = minterNameMap.get(minterName)
const cardTimestamp = card.updated || card.created || 0
const existingTimestamp = existingCard?.updated || existingCard?.created || 0
if (!existingCard || cardTimestamp > existingTimestamp) {
minterNameMap.set(minterName, card)
}
} }
// Sort final by newest overall
// Convert minterNameMap to final array finalOutput.sort((a, b) => {
const finalCards = [] const aTime = a.updated || a.created || 0
const seenMinterNames = new Set() const bTime = b.updated || b.created || 0
return bTime - aTime
for (const [minterName, card] of minterNameMap.entries()) {
if (!seenMinterNames.has(minterName)) {
finalCards.push(card)
seenMinterNames.add(minterName)
}
}
// Sort by timestamp descending
finalCards.sort((a, b) => {
const timestampA = a.updated || a.created || 0
const timestampB = b.updated || b.created || 0
return timestampB - timestampA
}) })
return finalCards return finalOutput
} }
@ -266,37 +379,51 @@ const loadCards = async (cardIdentifierPrefix) => {
const cardsContainer = document.getElementById("cards-container") const cardsContainer = document.getElementById("cards-container")
let isARBoard = false let isARBoard = false
cardsContainer.innerHTML = "<p>Loading cards...</p>" cardsContainer.innerHTML = "<p>Loading cards...</p>"
if ((cardIdentifierPrefix.startsWith(`QM-AR-card`))) {
if (cardIdentifierPrefix.startsWith("QM-AR-card")) {
isARBoard = true isARBoard = true
console.warn(`ARBoard determined:`, isARBoard) console.warn(`ARBoard determined:`, isARBoard)
} }
let afterTime = 0
const timeRangeSelect = document.getElementById("time-range-select")
if (timeRangeSelect) {
const days = parseInt(timeRangeSelect.value, 10)
if (days > 0) {
const now = Date.now()
const dayMs = 24 * 60 * 60 * 1000
afterTime = now - days * dayMs // e.g. last X days
console.log(`afterTime for last ${days} days = ${new Date(afterTime).toLocaleString()}`)
}
}
try { try {
const response = await searchSimple('BLOG_POST', `${cardIdentifierPrefix}`, '' , 0) // 1) Fetch raw "BLOG_POST" entries
const response = await searchSimple('BLOG_POST', cardIdentifierPrefix, '', 0, 0, '', false, true, afterTime)
if (!response || !Array.isArray(response) || response.length === 0) { if (!response || !Array.isArray(response) || response.length === 0) {
cardsContainer.innerHTML = "<p>No cards found.</p>" cardsContainer.innerHTML = "<p>No cards found.</p>"
return return
} }
// Validate cards and filter // 2) Validate structure
const validatedCards = await Promise.all( const validatedCards = await Promise.all(
response.map(async card => { response.map(async (card) => {
const isValid = await validateCardStructure(card) const isValid = await validateCardStructure(card)
return isValid ? card : null return isValid ? card : null
}) })
) )
const validCards = validatedCards.filter(card => card !== null) const validCards = validatedCards.filter((card) => card !== null)
if (validCards.length === 0) { if (validCards.length === 0) {
cardsContainer.innerHTML = "<p>No valid cards found.</p>" cardsContainer.innerHTML = "<p>No valid cards found.</p>"
return return
} }
const finalCards = await processMinterCards(validCards) // Additional logic for ARBoard or MinterCards
const finalCards = isARBoard
? await processARBoardCards(validCards)
: await processMinterBoardCards(validCards)
// finalCards is already sorted by newest (timestamp) by processMinterCards. // Sort finalCards according to selectedSort
// We'll re-sort if "Name" or "Recent Comments" is selected.
// Grab the selected sort from the dropdown (if it exists).
let selectedSort = 'newest' let selectedSort = 'newest'
const sortSelect = document.getElementById('sort-select') const sortSelect = document.getElementById('sort-select')
if (sortSelect) { if (sortSelect) {
@ -304,232 +431,202 @@ const loadCards = async (cardIdentifierPrefix) => {
} }
if (selectedSort === 'name') { if (selectedSort === 'name') {
// Sort alphabetically by the creator's name
finalCards.sort((a, b) => { finalCards.sort((a, b) => {
const nameA = a.name?.toLowerCase() || '' const nameA = a.name?.toLowerCase() || ''
const nameB = b.name?.toLowerCase() || '' const nameB = b.name?.toLowerCase() || ''
return nameA.localeCompare(nameB) return nameA.localeCompare(nameB)
}) })
} else if (selectedSort === 'recent-comments') { } else if (selectedSort === 'recent-comments') {
// We need each card's newest comment timestamp for sorting // If you need the newest comment timestamp
for (let card of finalCards) { for (let card of finalCards) {
card.newestCommentTimestamp = await getNewestCommentTimestamp(card.identifier) card.newestCommentTimestamp = await getNewestCommentTimestamp(card.identifier)
} }
// Then sort descending by newest comment
finalCards.sort((a, b) => finalCards.sort((a, b) =>
(b.newestCommentTimestamp || 0) - (a.newestCommentTimestamp || 0) (b.newestCommentTimestamp || 0) - (a.newestCommentTimestamp || 0)
) )
} else if (selectedSort === 'least-votes') { } else if (selectedSort === 'least-votes') {
// For each card, fetch its poll data so we know how many admin + minter votes it has. await applyVoteSortingData(finalCards, /* ascending= */ true)
// Store those values on the card object so we can sort on them.
// Sort ascending by admin votes; if there's a tie, sort ascending by minter votes.
const minterGroupMembers = await fetchMinterGroupMembers()
const minterAdmins = await fetchMinterGroupAdmins()
// For each card, fetch the poll data & store counts on the card object.
for (const card of finalCards) {
try {
// We must fetch the card data from QDN to get the `poll` name
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: card.name,
service: "BLOG_POST",
identifier: card.identifier,
})
// If the card or poll is missing, skip
if (!cardDataResponse || !cardDataResponse.poll) {
card._adminVotes = 0
card._adminYes = 0
card._minterVotes = 0
card._minterYes = 0
continue
}
const pollResults = await fetchPollResults(cardDataResponse.poll)
const {
adminYes,
adminNo,
minterYes,
minterNo
} = await processPollData(
pollResults,
minterGroupMembers,
minterAdmins,
cardDataResponse.creator,
card.identifier
)
// Store the totals so we can sort on them
card._adminVotes = adminYes + adminNo
card._adminYes = adminYes
card._minterVotes = minterYes + minterNo
card._minterYes = minterYes
} catch (error) {
console.warn(`Error fetching or processing poll for card ${card.identifier}:`, error)
// If something fails, default to zero so it sorts "lowest"
card._adminVotes = 0
card._adminYes = 0
card._minterVotes = 0
card._minterYes = 0
}
}
// Now that each card has _adminVotes + _minterVotes, do an ascending sort:
finalCards.sort((a, b) => {
// admin votes ascending
const diffAdminTotal = a._adminVotes - b._adminVotes
if (diffAdminTotal !== 0) return diffAdminTotal
// admin YES ascending
const diffAdminYes = a._adminYes - b._adminYes
if (diffAdminYes !== 0) return diffAdminYes
// minter votes ascending
const diffMinterTotal = a._minterVotes - b._minterVotes
if (diffMinterTotal !== 0) return diffMinterTotal
// minter YES ascending
return a._minterYes - b._minterYes
})
} else if (selectedSort === 'most-votes') { } else if (selectedSort === 'most-votes') {
// Fetch poll data & store admin + minter votes as before. await applyVoteSortingData(finalCards, /* ascending= */ false)
// Sort descending by admin votes; if there's a tie, sort descending by minter votes.
const minterGroupMembers = await fetchMinterGroupMembers()
const minterAdmins = await fetchMinterGroupAdmins()
for (const card of finalCards) {
try {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: card.name,
service: "BLOG_POST",
identifier: card.identifier,
})
if (!cardDataResponse || !cardDataResponse.poll) {
card._adminVotes = 0
card._adminYes = 0
card._minterVotes = 0
card._minterYes = 0
continue
}
const pollResults = await fetchPollResults(cardDataResponse.poll)
const {
adminYes,
adminNo,
minterYes,
minterNo
} = await processPollData(
pollResults,
minterGroupMembers,
minterAdmins,
cardDataResponse.creator,
card.identifier
)
card._adminVotes = adminYes + adminNo
card._adminYes = adminYes
card._minterVotes = minterYes + minterNo
card._minterYes = minterYes
} catch (error) {
console.warn(`Error fetching or processing poll for card ${card.identifier}:`, error)
card._adminVotes = 0
card._adminYes = 0
card._minterVotes = 0
card._minterYes = 0
}
}
// Sort descending by admin votes, then minter votes
finalCards.sort((a, b) => {
// admin votes descending
const diffAdminTotal = b._adminVotes - a._adminVotes
if (diffAdminTotal !== 0) return diffAdminTotal
// admin YES descending
const diffAdminYes = b._adminYes - a._adminYes
if (diffAdminYes !== 0) return diffAdminYes
// minter votes descending
const diffMinterTotal = b._minterVotes - a._minterVotes
if (diffMinterTotal !== 0) return diffMinterTotal
// minter YES descending
return b._minterYes - a._minterYes
})
} }
// else 'newest' => do nothing, finalCards stays in newest-first order // else 'newest' => do nothing (already sorted newest-first by your process functions).
// Create the 'finalCardsArray' that includes the data, etc.
let finalCardsArray = []
// Display skeleton cards immediately for (const card of finalCards) {
cardsContainer.innerHTML = ""
finalCards.forEach(card => {
const skeletonHTML = createSkeletonCardHTML(card.identifier)
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
})
// Fetch and update each card
finalCards.forEach(async card => {
try { try {
const cardDataResponse = await qortalRequest({ const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE", action: "FETCH_QDN_RESOURCE",
name: card.name, name: card.name,
service: "BLOG_POST", service: "BLOG_POST",
identifier: card.identifier, identifier: card.identifier
}) })
if (!cardDataResponse) {
console.warn(`Skipping invalid card: ${JSON.stringify(card)}`)
removeSkeleton(card.identifier)
return
}
if (!cardDataResponse.poll) {
console.warn(`Skipping card with no poll: ${card.identifier}`)
removeSkeleton(card.identifier)
return
}
// Getting the poll owner address uses the same API call as public key, so takes the same time, but address will be needed later. if (!cardDataResponse || !cardDataResponse.poll) {
// skip
console.warn(`Skipping card: missing data/poll. identifier=${card.identifier}`)
continue
}
// Extra validation: check poll ownership matches card publisher
const pollPublisherAddress = await getPollOwnerAddress(cardDataResponse.poll) const pollPublisherAddress = await getPollOwnerAddress(cardDataResponse.poll)
// Getting the card publisher address to compare instead should cause faster loading, since getting the public key by name first gets the address then converts it
const cardPublisherAddress = await fetchOwnerAddressFromName(card.name) const cardPublisherAddress = await fetchOwnerAddressFromName(card.name)
if (pollPublisherAddress !== cardPublisherAddress) {
if (pollPublisherAddress != cardPublisherAddress) { console.warn(`Poll hijack attack found, discarding card ${card.identifier}`)
console.warn(`not displaying card, QuickMythril pollHijack attack found! Discarding card with identifier: ${card.identifier}`) continue
removeSkeleton(card.identifier)
return
} }
const pollResults = await fetchPollResults(cardDataResponse.poll) // If ARBoard, do a quick address check
const bgColor = generateDarkPastelBackgroundBy(card.name)
const commentCount = await countComments(card.identifier)
const cardUpdatedTime = card.updated || null
if (isARBoard) { if (isARBoard) {
const name = await getNameInfo(cardDataResponse.minterName) const ok = await verifyARBoardAddress(cardDataResponse.minterName)
const address = name.owner if (!ok) {
if (minterAdminAddresses && minterGroupAddresses) { console.warn(`Invalid minter address for AR board. identifier=${card.identifier}`)
if (!minterAdminAddresses.includes(address) && !minterGroupAddresses.includes(address)) { continue
console.warn(`Found card from ARBoard that contained a non-minter!`)
removeSkeleton(card.identifier)
return
}
} else if (!minterAdminAddresses || !minterGroupAddresses){
const minterGroup = await fetchMinterGroupMembers()
const adminGroup = await fetchMinterGroupAdmins()
minterAdminAddresses = adminGroup.map(m => m.member)
minterGroupAddresses = minterGroup.map(m => m.member)
if (!minterAdminAddresses.includes(address) && !minterGroupAddresses.includes(address)) {
console.warn(`Found card from ARBoard that contained a non-minter!`)
removeSkeleton(card.identifier)
return
}
} }
} }
// **Push** to finalCardsArray for further processing (duplicates, etc.)
const finalCardHTML = isARBoard ? // If we're calling from the ARBoard, we will create HTML with a different function. finalCardsArray.push({
await createARCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, cardUpdatedTime, bgColor) ...card,
: cardDataResponse,
await createCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, cardUpdatedTime, bgColor, cardPublisherAddress) pollPublisherAddress,
replaceSkeleton(card.identifier, finalCardHTML) cardPublisherAddress,
})
} catch (error) { } catch (err) {
console.error(`Error processing card ${card.identifier}:`, error) console.error(`Error preparing card ${card.identifier}`, err)
removeSkeleton(card.identifier)
} }
}) }
// This will now allow duplicates to be displayed, but also mark the duplicates correctly. There is one bugged identifier that must be handled opposite.
// finalCardsArray = markDuplicates(finalCardsArray)
// Next, do the actual rendering:
cardsContainer.innerHTML = ""
for (const cardObj of finalCardsArray) {
// Insert a skeleton first if you like
const skeletonHTML = createSkeletonCardHTML(cardObj.identifier)
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
// Build final HTML
const pollResults = await fetchPollResults(cardObj.cardDataResponse.poll)
const commentCount = await countComments(cardObj.identifier)
const cardUpdatedTime = cardObj.updated || null
const bgColor = generateDarkPastelBackgroundBy(cardObj.name)
// Construct the final HTML for each card
const finalCardHTML = isARBoard
? await createARCardHTML(
cardObj.cardDataResponse,
pollResults,
cardObj.identifier,
commentCount,
cardUpdatedTime,
bgColor,
cardObj.cardPublisherAddress,
cardObj.isDuplicate
)
: await createCardHTML(
cardObj.cardDataResponse,
pollResults,
cardObj.identifier,
commentCount,
cardUpdatedTime,
bgColor,
cardObj.cardPublisherAddress
)
replaceSkeleton(cardObj.identifier, finalCardHTML)
}
} catch (error) { } catch (error) {
console.error("Error loading cards:", error) console.error("Error loading cards:", error)
cardsContainer.innerHTML = "<p>Failed to load cards.</p>" cardsContainer.innerHTML = "<p>Failed to load cards.</p>"
} }
} }
const verifyARBoardAddress = async (minterName) => {
try {
const nameInfo = await getNameInfo(minterName)
if (!nameInfo) return false
const minterAddress = nameInfo.owner
const isValid = await getAddressInfo(minterAddress)
if (!isValid) return false
// Then check if they're in the minter group
const minterGroup = await fetchMinterGroupMembers()
const adminGroup = await fetchMinterGroupAdmins()
const minterGroupAddresses = minterGroup.map(m => m.member)
const adminGroupAddresses = adminGroup.map(m => m.member)
return (minterGroupAddresses.includes(minterAddress) ||
adminGroupAddresses.includes(minterAddress))
} catch (err) {
console.warn("verifyARBoardAddress error:", err)
return false
}
}
const applyVoteSortingData = async (cards, ascending = true) => {
const minterGroupMembers = await fetchMinterGroupMembers()
const minterAdmins = await fetchMinterGroupAdmins()
for (const card of cards) {
try {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: card.name,
service: "BLOG_POST",
identifier: card.identifier,
})
if (!cardDataResponse || !cardDataResponse.poll) {
card._adminVotes = 0
card._adminYes = 0
card._minterVotes = 0
card._minterYes = 0
continue
}
const pollResults = await fetchPollResults(cardDataResponse.poll);
const { adminYes, adminNo, minterYes, minterNo } = await processPollData(
pollResults,
minterGroupMembers,
minterAdmins,
cardDataResponse.creator,
card.identifier
)
card._adminVotes = adminYes + adminNo
card._adminYes = adminYes
card._minterVotes = minterYes + minterNo
card._minterYes = minterYes
} catch (error) {
console.warn(`Error fetching or processing poll for card ${card.identifier}:`, error)
card._adminVotes = 0
card._adminYes = 0
card._minterVotes = 0
card._minterYes = 0
}
}
if (ascending) {
// least votes first
cards.sort((a, b) => {
const diffAdminTotal = a._adminVotes - b._adminVotes
if (diffAdminTotal !== 0) return diffAdminTotal
const diffAdminYes = a._adminYes - b._adminYes
if (diffAdminYes !== 0) return diffAdminYes
const diffMinterTotal = a._minterVotes - b._minterVotes
if (diffMinterTotal !== 0) return diffMinterTotal
return a._minterYes - b._minterYes
})
} else {
// most votes first
cards.sort((a, b) => {
const diffAdminTotal = b._adminVotes - a._adminVotes
if (diffAdminTotal !== 0) return diffAdminTotal
const diffAdminYes = b._adminYes - a._adminYes
if (diffAdminYes !== 0) return diffAdminYes
const diffMinterTotal = b._minterVotes - a._minterVotes
if (diffMinterTotal !== 0) return diffMinterTotal
return b._minterYes - a._minterYes
})
}
}
const removeSkeleton = (cardIdentifier) => { const removeSkeleton = (cardIdentifier) => {
const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`) const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`)
if (skeletonCard) { if (skeletonCard) {

View File

@ -802,9 +802,9 @@ const searchAllWithOffset = async (service, query, limit, offset, room) => {
} }
} }
// NOTE - This function does a search and will return EITHER AN ARRAY OR A SINGLE OBJECT. if you want to guarantee a single object, pass 1 as limit. i.e. await searchSimple(service, identifier, "", 1) will return a single object. // NOTE - This function does a search and will return EITHER AN ARRAY OR A SINGLE OBJECT. if you want to guarantee a single object, pass 1 as limit. i.e. await searchSimple(service, identifier, "", 1) will return a single object.
const searchSimple = async (service, identifier, name, limit=1500, offset=0, room='', reverse=true, prefixOnly=true) => { const searchSimple = async (service, identifier, name, limit=1500, offset=0, room='', reverse=true, prefixOnly=true, after=0) => {
try { try {
let urlSuffix = `service=${service}&identifier=${identifier}&name=${name}&prefix=true&limit=${limit}&offset=${offset}&reverse=${reverse}&prefix=${prefixOnly}` let urlSuffix = `service=${service}&identifier=${identifier}&name=${name}&prefix=true&limit=${limit}&offset=${offset}&reverse=${reverse}&prefix=${prefixOnly}&fter=${after}`
if (name && !identifier && !room) { if (name && !identifier && !room) {
console.log('name only searchSimple', name) console.log('name only searchSimple', name)

View File

@ -42,7 +42,7 @@
</a> </a>
</span> </span>
<span class="navbar-caption-wrap"> <span class="navbar-caption-wrap">
<a class="navbar-caption display-4" href="index.html">Q-Mintership (v1.02b) <a class="navbar-caption display-4" href="index.html">Q-Mintership (v1.04b)
</a> </a>
</span> </span>
</div> </div>
@ -61,12 +61,12 @@
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt=""> <img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
</a> </a>
</span> </span>
<span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html">Q-Mintership v1.02b<br></a></span> <span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html">Q-Mintership v1.04b<br></a></span>
</div> </div>
<ul class="navbar-nav nav-dropdown" data-app-modern-menu="true"><li class="nav-item"><a class="nav-link link text-primary display-7" href="MINTERSHIP-FORUM"></a></li></ul> <ul class="navbar-nav nav-dropdown" data-app-modern-menu="true"><li class="nav-item"><a class="nav-link link text-primary display-7" href="MINTERSHIP-FORUM"></a></li></ul>
<div class="mbr-section-btn-main" role="tablist"><a class="btn btn-danger display-4" href="MINTERSHIP-FORUM">FORUM<br></a> <a class="btn admin-btn btn-secondary display-4" href="TOOLS">ADMIN TOOLS</a><a class="btn admin-btn btn-secondary display-4" href="ADMINBOARD">ADMIN BOARD</a><a class="btn btn-danger display-4" href="MINTERS">MINTER BOARD</a><a class="btn btn-danger display-4" href="ADDREMOVEADMIN">ARA BOARD</a></div> <div class="mbr-section-btn-main" role="tablist"><a class="btn btn-danger display-4" href="MINTERSHIP-FORUM">FORUM<br></a> <a class="btn admin-btn btn-secondary display-4" href="TOOLS">ADMIN TOOLS</a><a class="btn admin-btn btn-secondary display-4" href="ADMINBOARD">ADMIN BOARD</a><a class="btn btn-danger display-4" href="MINTERS">MINTER BOARD</a><a class="btn btn-danger display-4" href="ADDREMOVEADMIN">MAM Board</a></div>
</div> </div>
</div> </div>
@ -93,7 +93,7 @@
<div class="item-wrapper"> <div class="item-wrapper">
<img src="assets/images/mbr-1623x1112.jpg" alt="Mintership Forum" data-slide-to="0" data-bs-slide-to="0"> <img src="assets/images/mbr-1623x1112.jpg" alt="Mintership Forum" data-slide-to="0" data-bs-slide-to="0">
<div class="item-content"> <div class="item-content">
<h2 class="card-title mbr-fonts-style display-2">Minting Rights? - Start Here - MinterBoard</h2> <h2 class="card-title mbr-fonts-style display-2">MinterBoard</h2>
</div> </div>
</div> </div>
</a> </a>
@ -109,7 +109,7 @@
<img src="assets/images/mbr-1818x1212.jpg" alt="Admin Board" data-slide-to="1" data-bs-slide-to="1"> <img src="assets/images/mbr-1818x1212.jpg" alt="Admin Board" data-slide-to="1" data-bs-slide-to="1">
<div class="item-content"> <div class="item-content">
<h2 class="card-title mbr-fonts-style display-2"> <h2 class="card-title mbr-fonts-style display-2">
Promote / Demote Minter Admins</h2> Minter Admin Management (MAM) Board</h2>
</div> </div>
</div> </div>
</a> </a>
@ -191,6 +191,42 @@
<section data-bs-version="5.1" class="content7 boldm5 cid-uufIRKtXOO" id="content7-6"> <section data-bs-version="5.1" class="content7 boldm5 cid-uufIRKtXOO" id="content7-6">
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
v1.04beta 01-27-2025</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
<b><u>v1.04b Fixes</u></b>- <b>MANY fixes </b> - See post in the <a href="MINTERSHIP-FORUM">FORUM</a> for details, too many to list here.
</p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
v1.03beta 01-23-2025</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
<b><u>v1.03b Fixes</u></b>- <b>Filtering issue resolved </b> - Version 1.02 had a filtering logic modification applied to add and remove admin transactions. However, it was not changed on the REMOVE filtering, so REMOVE transactions that were PENDING were showing as COMPLETE and thus the board was displaying cards as already removed when there was only a PENDING tx. This has been resolved in 1.03b.
</p>
</div>
</div>
</div>
</div>
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12 col-lg-7 card"> <div class="col-12 col-lg-7 card">
@ -536,12 +572,12 @@
<div class="title-wrapper"> <div class="title-wrapper">
<div class="title-wrap"> <div class="title-wrap">
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt=""> <img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
<h2 class="mbr-section-title mbr-fonts-style display-5">Q-Mintership (v1.02b)</h2> <h2 class="mbr-section-title mbr-fonts-style display-5">Q-Mintership (v1.04b)</h2>
</div> </div>
</div> </div>
<a class="link-wrap" href="#"> <a class="link-wrap" href="#">
<p class="mbr-link mbr-fonts-style display-4">Q-Mintership v1.02beta</p> <p class="mbr-link mbr-fonts-style display-4">Q-Mintership v1.04beta</p>
</a> </a>
</div> </div>
<div class="col-12 col-lg-6"> <div class="col-12 col-lg-6">