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;">
<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>
<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 id="cards-container" class="cards-container" style="margin-top: 1rem"">
<!-- We'll fill this with existing proposal cards -->
@ -80,8 +87,8 @@ const loadAddRemoveAdminPage = async () => {
// proposeButton.style.display === 'flex' ? 'none' : 'flex'
} catch (error) {
console.error("Error checking for existing card:", error)
alert("Failed to check for existing card. Please try again.")
console.error("Error opening propose form", error)
alert("Failed to open proposal form. Please try again.")
}
})
@ -126,79 +133,79 @@ const toggleProposeButton = () => {
proposeButton.style.display === 'flex' ? 'none' : 'flex'
}
let addAdminTxs
let remAdminTxs
const fetchAllARTxData = async () => {
const addAdmTx = "ADD_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({
txTypes: [addAdmTx],
confirmationStatus: 'CONFIRMED',
limit: 0,
reverse: true,
offset: 0,
startBlock: 1990000,
blockLimit: 0,
txGroupId: 694,
})
// Filter out 'PENDING'
addAdminTxs = filterAddTransactions(allAddTxs)
console.warn('addAdminTxData (no PENDING nor past+PENDING):', addAdminTxs)
txTypes: [addAdmTx],
confirmationStatus: 'CONFIRMED',
limit: 0,
reverse: true,
offset: 0,
startBlock: 1990000,
blockLimit: 0,
txGroupId: 694,
})
const allRemTxs = await searchTransactions({
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 adminListContainer = document.getElementById("admin-list-container")
@ -309,9 +316,10 @@ const handleProposeDemotion = async (adminName, adminAddress) => {
// Notify the user to fill out the rest
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 {
const response = await searchSimple(
'BLOG_POST',
@ -381,7 +389,7 @@ const handleProposeDemotion = async (adminName, adminAddress) => {
console.error("Error fetching existing AR card:", error)
return null
}
}
}
const publishARCard = async (cardIdentifierPrefix) => {
@ -726,8 +734,8 @@ const fallbackMinterCheck = async (minterName, minterGroupMembers, minterAdmins)
}
const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => {
const { minterName, header, content, links, creator, timestamp, poll, promotionCard } = cardData
const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, bgColor, cardPublisherAddress, illegalDuplicate) => {
const { minterName, minterAddress='priorToAddition', header, content, links, creator, timestamp, poll, promotionCard } = cardData
const formattedDate = new Date(timestamp).toLocaleString()
const minterAvatar = await getMinterAvatar(minterName)
const creatorAvatar = await getMinterAvatar(creator)
@ -744,7 +752,7 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
// showPromotionCard = await fallbackMinterCheck(minterName, minterGroupMembers, minterAdmins)
if (typeof promotionCard === 'boolean') {
showPromotionCard = promotionCard;
showPromotionCard = promotionCard
} else if (typeof promotionCard === 'string') {
// Could be "true" or "false" or something else
const lower = promotionCard.trim().toLowerCase()
@ -790,41 +798,63 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
let altText = ''
const verifiedName = await validateMinterName(minterName)
if (verifiedName) {
if (verifiedName && !illegalDuplicate) {
const accountInfo = await getNameInfo(verifiedName)
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}`)
const actionsHtmlCheck = await checkAndDisplayActions(adminYes, verifiedName, cardIdentifier)
actionsHtml = actionsHtmlCheck
if (!addAdminTxs || !remAdminTxs) {
await fetchAllARTxData()
}
const { finalAddTxs, pendingAddTxs, finalRemTxs, pendingRemTxs } = await fetchAllARTxData()
if (addAdminTxs.some((addTx) => addTx.groupId === 694 && addTx.member === accountAddress)){
console.warn(`account was already adminified(PROMOTED), displaying as such...`)
const confirmedAdd = finalAddTxs.some(
(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)'
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 = ''
// =============================================================== if 'showPromotedDemoted' is wanted to be added.
// if (!showPromotedDemoted){
// console.warn(`promoted/demoted show checkbox is unchecked, and card is promoted, not displaying...`)
// return ''
// }
}
if (confirmedAdd && userPendingRemove && existingAdmin) {
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)){
console.warn(`account was already UNadminified(DEMOTED), displaying as such...`)
// If user has a final "remove" and no pending additions or removals
if (confirmedRemove && !userPendingAdd && existingMinter) {
console.warn(`account was demoted, final. no add pending, existingMinter.`);
cardColorCode = 'rgb(29, 4, 6)'
altText = `<h4 style="color:rgb(73, 24, 24); margin-bottom: 0.5em;">DEMOTED from ADMIN</h4>`
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 {
console.warn(`name could not be validated, not setting actionsHtml`)
actionsHtml = ''

View File

@ -83,6 +83,13 @@ const loadAdminBoardPage = async () => {
<option value="least-votes">Least Votes</option>
<option value="most-votes">Most Votes</option>
</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;">
<input type="checkbox" id="admin-show-hidden-checkbox" name="adminHidden" />
<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")
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 {
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) {
encryptedCardsContainer.innerHTML = "<p>No cards found.</p>"
@ -379,12 +398,13 @@ const fetchAllEncryptedCards = async (isRefresh = false) => {
const latestCardsMap = new Map()
validCardsWithData.forEach(({ card, decryptedCardData }) => {
const timestamp = card.updated || card.created || 0
const timestamp = card.created || 0
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 })
}
}
})
const uniqueValidCards = Array.from(latestCardsMap.values())
@ -396,13 +416,11 @@ const fetchAllEncryptedCards = async (isRefresh = false) => {
const obtainedMinterName = decryptedCardData.minterName
// Only check for cards that are NOT topic-based cards
if ((!decryptedCardData.isTopic) || decryptedCardData.isTopic === 'false') {
const cardTimestamp = card.updated || card.created || 0
if (obtainedMinterName) {
const existingEntry = mostRecentCardsMap.get(obtainedMinterName)
// Replace only if the current card is more recent
if (!existingEntry || cardTimestamp > (existingEntry.card.updated || existingEntry.card.created || 0)) {
if (!existingEntry) {
mostRecentCardsMap.set(obtainedMinterName, { card, decryptedCardData })
}
}
@ -414,7 +432,7 @@ const fetchAllEncryptedCards = async (isRefresh = false) => {
})
// Convert the map into an array of final cards
const finalCards = Array.from(mostRecentCardsMap.values());
const finalCards = Array.from(mostRecentCardsMap.values())
let selectedSort = 'newest'
const sortSelect = document.getElementById('sort-select')
@ -1037,7 +1055,7 @@ const processQortalLinkForRendering = async (link) => {
return link
}
const checkAndDisplayRemoveActions = async (adminYes, name, cardIdentifier) => {
const checkAndDisplayRemoveActions = async (adminYes, name, cardIdentifier, nameIsActuallyAddress = false) => {
const latestBlockInfo = await getLatestBlockInfo()
const isBlockPassed = latestBlockInfo.height >= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT
let minAdminCount
@ -1052,10 +1070,15 @@ const checkAndDisplayRemoveActions = async (adminYes, name, cardIdentifier) => {
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}`)
}
if (isBlockPassed && userState.isMinterAdmin) {
if (isBlockPassed && (userState.isMinterAdmin || userState.isAdmin)) {
console.warn(`feature trigger has passed, checking for approval requirements`)
const addressInfo = await getNameInfo(name)
const address = addressInfo.owner
let address
if (!nameIsActuallyAddress){
const nameInfo = await getNameInfo(name)
address = nameInfo.owner
} else {
address = name
}
const kickApprovalHtml = await checkGroupApprovalAndCreateButton(address, cardIdentifier, "GROUP_KICK")
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)
return removeButtonHtml
} else{
@ -1098,6 +1121,8 @@ const createRemoveButtonHtml = (name, cardIdentifier) => {
const handleKickMinter = async (minterName) => {
try {
isAddress = await getAddressInfo(minterName)
// Optional block check
let txGroupId = 0
// const { height: currentHeight } = await getLatestBlockInfo()
@ -1108,8 +1133,14 @@ const handleKickMinter = async (minterName) => {
}
// Get the minter address from name info
const minterNameInfo = await getNameInfo(minterName)
const minterAddress = minterNameInfo?.owner
let minterAddress
if (!isAddress){
const minterNameInfo = await getNameInfo(minterName)
minterAddress = minterNameInfo?.owner
} else {
minterAddress = minterName
}
if (!minterAddress) {
alert(`No valid address found for minter name: ${minterName}, this should NOT have happened, please report to developers...`)
return
@ -1150,6 +1181,7 @@ const handleKickMinter = async (minterName) => {
}
const handleBanMinter = async (minterName) => {
isAddress = await getAddressInfo(minterName)
try {
let txGroupId = 0
// const { height: currentHeight } = await getLatestBlockInfo()
@ -1161,9 +1193,13 @@ const handleBanMinter = async (minterName) => {
console.log(`featureTrigger block is passed, using txGroupId 694`)
txGroupId = 694
}
const minterNameInfo = await getNameInfo(minterName)
const minterAddress = minterNameInfo?.owner
let minterAddress
if (!isAddress) {
const minterNameInfo = await getNameInfo(minterName)
const minterAddress = minterNameInfo?.owner
} else {
minterAddress = minterName
}
if (!minterAddress) {
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 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')
@ -1254,7 +1290,7 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
let cardColorCode = showTopic ? '#0e1b15' : '#151f28'
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}` :
`
<div class="support-header"><h5> REGARDING (Name): </h5></div>
@ -1274,16 +1310,23 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
let adjustmentText = ''
const verifiedName = await validateMinterName(minterName)
let levelText = '</h3>'
const addressVerification = await getAddressInfo(minterName)
const verifiedAddress = addressVerification.address
if (verifiedName) {
const accountInfo = await getNameInfo(verifiedName)
const accountAddress = accountInfo.owner
const addressInfo = await getAddressInfo(accountAddress)
if (verifiedName || verifiedAddress) {
let accountInfo
if (!verifiedAddress){
accountInfo = await getNameInfo(verifiedName)
}
const accountAddress = verifiedAddress ? addressVerification.address : accountInfo.owner
const addressInfo = verifiedAddress ? addressVerification : await getAddressInfo(accountAddress)
levelText = ` - Level ${addressInfo.level}</h3>`
console.log(`name is validated, utilizing for removal features...${verifiedName}`)
penaltyText = addressInfo.blocksMintedPenalty == 0 ? '' : '<p>(has Blocks Penalty)<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
if (userVote === 0) {
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;">
<span>${userState.accountName || 'Guest'}</span>
</div>
<div><h2>No Functionality Here Yet</h2></div>
<div><h2>COMING SOON...</h2></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 until then is simply a placeholder.</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> 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>

View File

@ -37,6 +37,13 @@ const loadMinterBoardPage = async () => {
<option value="least-votes">Least Votes</option>
<option value="most-votes">Most Votes</option>
</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="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
<form id="publish-card-form">
@ -127,6 +134,11 @@ const loadMinterBoardPage = async () => {
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 () => {
// Re-load the cards whenever user chooses a new sort option.
await loadCards(minterCardIdentifierPrefix)
@ -149,11 +161,11 @@ const extractMinterCardsMinterName = async (cardIdentifier) => {
}
try {
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
return minterName
} 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 cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
@ -161,103 +173,204 @@ const extractMinterCardsMinterName = async (cardIdentifier) => {
service: "BLOG_POST",
identifier: cardIdentifier,
})
let nameInvalid = false
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) {
throw error
}
}
const processMinterCards = async (validMinterCards) => {
const latestCardsMap = new Map()
// Deduplicate by identifier, keeping the most recent
validMinterCards.forEach(card => {
const timestamp = card.updated || card.created || 0
const existingCard = latestCardsMap.get(card.identifier)
if (!existingCard || timestamp > (existingCard.updated || existingCard.created || 0)) {
latestCardsMap.set(card.identifier, card)
const groupAndLabelByIdentifier = (allCards) => {
// Group by identifier
const mapById = new Map()
allCards.forEach(card => {
if (!mapById.has(card.identifier)) {
mapById.set(card.identifier, [])
}
mapById.get(card.identifier).push(card)
})
// Convert Map back to array
const uniqueValidCards = Array.from(latestCardsMap.values())
// For each identifier's group, sort oldest->newest so the first is "master"
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()
const minterGroupAddresses = minterGroupMembers.map(m => m.member)
const minterNameMap = new Map()
// Mark the first as master
group[0].isMaster = true
// The rest are updates
for (let i = 1; i < group.length; i++) {
group[i].isMaster = false
}
// For each card, extract minterName safely
for (const card of uniqueValidCards) {
let minterName
// push them all to output
output.push(...group)
}
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 {
// If this throws, we catch below and skip
minterName = await extractMinterCardsMinterName(card.identifier)
} catch (error) {
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.`)
masterMinterName = await extractMinterCardsMinterName(masterCard.identifier)
} catch (err) {
console.warn(`Skipping entire group ${identifier}, no valid minterName from master`, err)
continue
}
const minterAddress = minterNameInfo.owner
// Validate the address
const addressValid = await getAddressInfo(minterAddress)
if (!minterAddress || !addressValid) {
console.warn(`minterAddress invalid or missing for: ${minterName}, skipping card.`, minterAddress)
continue
}
// Store an object with the minterName we extracted, plus all cards in that group
nameGroups.push({
minterName: masterMinterName,
cards: group // includes the master & updates
})
}
// If this is a 'regular' minter card, skip if user is already a minter
if (!card.identifier.includes('QM-AR-card')) {
if (minterGroupAddresses.includes(minterAddress)) {
console.log(
`existing minter found or fake name detected. Not including minter card: ${card.identifier}`
)
continue
// Combine them: minterName => array of *all* cards from all matching groups
const combinedMap = new Map()
for (const entry of nameGroups) {
const mName = entry.minterName
if (!combinedMap.has(mName)) {
combinedMap.set(mName, [])
}
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
}
}
// Keep only the most recent card for each minterName
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)
}
// push them all
finalOutput.push(...group)
}
// Convert minterNameMap to final array
const finalCards = []
const seenMinterNames = new Set()
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
// Sort final by newest overall
finalOutput.sort((a, b) => {
const aTime = a.updated || a.created || 0
const bTime = b.updated || b.created || 0
return bTime - aTime
})
return finalCards
return finalOutput
}
@ -266,37 +379,51 @@ const loadCards = async (cardIdentifierPrefix) => {
const cardsContainer = document.getElementById("cards-container")
let isARBoard = false
cardsContainer.innerHTML = "<p>Loading cards...</p>"
if ((cardIdentifierPrefix.startsWith(`QM-AR-card`))) {
if (cardIdentifierPrefix.startsWith("QM-AR-card")) {
isARBoard = true
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 {
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) {
cardsContainer.innerHTML = "<p>No cards found.</p>"
return
}
// Validate cards and filter
// 2) Validate structure
const validatedCards = await Promise.all(
response.map(async card => {
response.map(async (card) => {
const isValid = await validateCardStructure(card)
return isValid ? card : null
})
)
const validCards = validatedCards.filter(card => card !== null)
const validCards = validatedCards.filter((card) => card !== null)
if (validCards.length === 0) {
cardsContainer.innerHTML = "<p>No valid cards found.</p>"
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.
// We'll re-sort if "Name" or "Recent Comments" is selected.
// Grab the selected sort from the dropdown (if it exists).
// Sort finalCards according to selectedSort
let selectedSort = 'newest'
const sortSelect = document.getElementById('sort-select')
if (sortSelect) {
@ -304,232 +431,202 @@ const loadCards = async (cardIdentifierPrefix) => {
}
if (selectedSort === 'name') {
// Sort alphabetically by the creator's name
finalCards.sort((a, b) => {
const nameA = a.name?.toLowerCase() || ''
const nameB = b.name?.toLowerCase() || ''
return nameA.localeCompare(nameB)
})
} 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) {
card.newestCommentTimestamp = await getNewestCommentTimestamp(card.identifier)
}
// Then sort descending by newest comment
finalCards.sort((a, b) =>
(b.newestCommentTimestamp || 0) - (a.newestCommentTimestamp || 0)
)
} else if (selectedSort === 'least-votes') {
// For each card, fetch its poll data so we know how many admin + minter votes it has.
// 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
})
await applyVoteSortingData(finalCards, /* ascending= */ true)
} else if (selectedSort === 'most-votes') {
// Fetch poll data & store admin + minter votes as before.
// 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
})
await applyVoteSortingData(finalCards, /* ascending= */ false)
}
// 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
cardsContainer.innerHTML = ""
finalCards.forEach(card => {
const skeletonHTML = createSkeletonCardHTML(card.identifier)
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
})
// Fetch and update each card
finalCards.forEach(async card => {
for (const card of finalCards) {
try {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: card.name,
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)
// 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)
if (pollPublisherAddress != cardPublisherAddress) {
console.warn(`not displaying card, QuickMythril pollHijack attack found! Discarding card with identifier: ${card.identifier}`)
removeSkeleton(card.identifier)
return
if (pollPublisherAddress !== cardPublisherAddress) {
console.warn(`Poll hijack attack found, discarding card ${card.identifier}`)
continue
}
const pollResults = await fetchPollResults(cardDataResponse.poll)
const bgColor = generateDarkPastelBackgroundBy(card.name)
const commentCount = await countComments(card.identifier)
const cardUpdatedTime = card.updated || null
// If ARBoard, do a quick address check
if (isARBoard) {
const name = await getNameInfo(cardDataResponse.minterName)
const address = name.owner
if (minterAdminAddresses && minterGroupAddresses) {
if (!minterAdminAddresses.includes(address) && !minterGroupAddresses.includes(address)) {
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
}
const ok = await verifyARBoardAddress(cardDataResponse.minterName)
if (!ok) {
console.warn(`Invalid minter address for AR board. identifier=${card.identifier}`)
continue
}
}
const finalCardHTML = isARBoard ? // If we're calling from the ARBoard, we will create HTML with a different function.
await createARCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, cardUpdatedTime, bgColor)
:
await createCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, cardUpdatedTime, bgColor, cardPublisherAddress)
replaceSkeleton(card.identifier, finalCardHTML)
} catch (error) {
console.error(`Error processing card ${card.identifier}:`, error)
removeSkeleton(card.identifier)
// **Push** to finalCardsArray for further processing (duplicates, etc.)
finalCardsArray.push({
...card,
cardDataResponse,
pollPublisherAddress,
cardPublisherAddress,
})
} catch (err) {
console.error(`Error preparing card ${card.identifier}`, err)
}
})
}
// 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) {
console.error("Error loading cards:", error)
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 skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`)
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.
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 {
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) {
console.log('name only searchSimple', name)

View File

@ -42,7 +42,7 @@
</a>
</span>
<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>
</span>
</div>
@ -61,12 +61,12 @@
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
</a>
</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>
<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>
@ -93,7 +93,7 @@
<div class="item-wrapper">
<img src="assets/images/mbr-1623x1112.jpg" alt="Mintership Forum" data-slide-to="0" data-bs-slide-to="0">
<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>
</a>
@ -109,7 +109,7 @@
<img src="assets/images/mbr-1818x1212.jpg" alt="Admin Board" data-slide-to="1" data-bs-slide-to="1">
<div class="item-content">
<h2 class="card-title mbr-fonts-style display-2">
Promote / Demote Minter Admins</h2>
Minter Admin Management (MAM) Board</h2>
</div>
</div>
</a>
@ -191,6 +191,42 @@
<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="row">
<div class="col-12 col-lg-7 card">
@ -536,12 +572,12 @@
<div class="title-wrapper">
<div class="title-wrap">
<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>
<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>
</div>
<div class="col-12 col-lg-6">