Module:MaintenanceDashboard
Documentation for this module may be created at Module:MaintenanceDashboard/doc
-- Module:MaintenanceDashboard
-- Generates status cards for various maintenance checks across the wiki
local p = {}
local function trim(s)
if s == nil then return nil end
s = tostring(s)
s = s:gsub('^%s+', ''):gsub('%s+$', '')
if s == '' then return nil end
return s
end
-- Get count from an SMW query
local function getCount(frame, query)
local result = frame:preprocess('{{#ask:' .. query .. '|format=count}}')
return tonumber(trim(result)) or 0
end
-- Generate a single status card
-- If fixedStatus is provided, it overrides threshold-based status
local function statusCard(label, count, link, threshold_warning, threshold_error, fixedStatus)
threshold_warning = threshold_warning or 1
threshold_error = threshold_error or 10
local status
if fixedStatus then
status = fixedStatus
else
status = 'ok'
if count >= threshold_error then
status = 'error'
elseif count >= threshold_warning then
status = 'warning'
end
end
local linkAttr = ''
if link then
-- Add colon prefix for Category: links so they render as links, not category assignments
local linkValue = link
if link:match('^Category:') then
linkValue = ':' .. link
end
linkAttr = '|link=' .. linkValue
end
return '{{Maintenance status card|label=' .. label .. '|count=' .. count .. '|status=' .. status .. linkAttr .. '}}'
end
-- Count missing brand pages (brands used but no wiki page exists)
local function countMissingBrandPages(frame)
local missingCount = 0
local checkedBrands = {}
-- Get all brands from CRT models
local crtBrandQuery = frame:preprocess('{{#ask:[[Category:CRT models]][[Has brand::+]]|?Has brand|format=broadtable|headers=hide|link=none|mainlabel=-|limit=500|searchlabel=}}')
if crtBrandQuery then
for cell in crtBrandQuery:gmatch('<td[^>]*>(.-)</td>') do
local brand = trim(cell:gsub('<[^>]+>', ''))
if brand and brand ~= '' and not checkedBrands[brand] then
checkedBrands[brand] = true
local title = mw.title.new(brand)
if title and not title.exists then
missingCount = missingCount + 1
end
end
end
end
-- Get all brands from AV devices
local avBrandQuery = frame:preprocess('{{#ask:[[Category:AV devices]][[Has brand::+]]|?Has brand|format=broadtable|headers=hide|link=none|mainlabel=-|limit=500|searchlabel=}}')
if avBrandQuery then
for cell in avBrandQuery:gmatch('<td[^>]*>(.-)</td>') do
local brand = trim(cell:gsub('<[^>]+>', ''))
if brand and brand ~= '' and not checkedBrands[brand] then
checkedBrands[brand] = true
local title = mw.title.new(brand)
if title and not title.exists then
missingCount = missingCount + 1
end
end
end
end
return missingCount
end
-- Category maintenance status grid
function p.categoryStatus(frame)
-- Count missing category pages using the audit logic
local missingCRTBrands = 0
local missingCRTTypes = 0
local missingAVBrands = 0
local missingAVTypes = 0
-- Get used CRT brands
local usedCRTBrands = {}
local brandQuery = frame:preprocess('{{#ask:[[Category:CRT models]][[Has brand::+]]|?Has brand|format=broadtable|headers=hide|link=none|mainlabel=-|limit=500|searchlabel=}}')
if brandQuery then
for cell in brandQuery:gmatch('<td[^>]*>(.-)</td>') do
local brand = trim(cell:gsub('<[^>]+>', ''))
if brand and brand ~= '' then
usedCRTBrands[brand] = true
end
end
end
-- Get registered CRT brand categories
local registeredCRTBrands = {}
local regQuery = frame:preprocess('{{#ask:[[Is CRT brand category::Yes]]|?Brand name|format=broadtable|headers=hide|link=none|mainlabel=-|limit=500|searchlabel=}}')
if regQuery then
for cell in regQuery:gmatch('<td[^>]*>(.-)</td>') do
local brand = trim(cell:gsub('<[^>]+>', ''))
if brand and brand ~= '' then
registeredCRTBrands[brand] = true
end
end
end
for brand, _ in pairs(usedCRTBrands) do
if not registeredCRTBrands[brand] then
missingCRTBrands = missingCRTBrands + 1
end
end
-- Get used CRT types
local usedCRTTypes = {}
local typeQuery = frame:preprocess('{{#ask:[[Category:CRT models]][[Has CRT type::+]]|?Has CRT type|format=broadtable|headers=hide|link=none|mainlabel=-|limit=500|searchlabel=}}')
if typeQuery then
for cell in typeQuery:gmatch('<td[^>]*>(.-)</td>') do
local crtType = trim(cell:gsub('<[^>]+>', ''))
if crtType and crtType ~= '' then
usedCRTTypes[crtType] = true
end
end
end
-- Get registered CRT type categories
local registeredCRTTypes = {}
local regTypeQuery = frame:preprocess('{{#ask:[[Is CRT type category::Yes]]|?CRT type name|format=broadtable|headers=hide|link=none|mainlabel=-|limit=500|searchlabel=}}')
if regTypeQuery then
for cell in regTypeQuery:gmatch('<td[^>]*>(.-)</td>') do
local crtType = trim(cell:gsub('<[^>]+>', ''))
if crtType and crtType ~= '' then
registeredCRTTypes[crtType] = true
end
end
end
for crtType, _ in pairs(usedCRTTypes) do
if not registeredCRTTypes[crtType] then
missingCRTTypes = missingCRTTypes + 1
end
end
-- Get used AV brands
local usedAVBrands = {}
local avBrandQuery = frame:preprocess('{{#ask:[[Category:AV devices]][[Has brand::+]]|?Has brand|format=broadtable|headers=hide|link=none|mainlabel=-|limit=500|searchlabel=}}')
if avBrandQuery then
for cell in avBrandQuery:gmatch('<td[^>]*>(.-)</td>') do
local brand = trim(cell:gsub('<[^>]+>', ''))
if brand and brand ~= '' then
usedAVBrands[brand] = true
end
end
end
-- Get registered AV brand categories
local registeredAVBrands = {}
local regAVQuery = frame:preprocess('{{#ask:[[Is AV brand category::Yes]]|?Brand name|format=broadtable|headers=hide|link=none|mainlabel=-|limit=500|searchlabel=}}')
if regAVQuery then
for cell in regAVQuery:gmatch('<td[^>]*>(.-)</td>') do
local brand = trim(cell:gsub('<[^>]+>', ''))
if brand and brand ~= '' then
registeredAVBrands[brand] = true
end
end
end
for brand, _ in pairs(usedAVBrands) do
if not registeredAVBrands[brand] then
missingAVBrands = missingAVBrands + 1
end
end
-- Get used AV types
local usedAVTypes = {}
local avTypeQuery = frame:preprocess('{{#ask:[[Category:AV devices]][[Has device type::+]]|?Has device type|format=broadtable|headers=hide|link=none|mainlabel=-|limit=500|searchlabel=}}')
if avTypeQuery then
for cell in avTypeQuery:gmatch('<td[^>]*>(.-)</td>') do
local deviceType = trim(cell:gsub('<[^>]+>', ''))
if deviceType and deviceType ~= '' then
usedAVTypes[deviceType] = true
end
end
end
-- Get registered AV type categories
local registeredAVTypes = {}
local regAVTypeQuery = frame:preprocess('{{#ask:[[Is AV device type category::Yes]]|?Device type name|format=broadtable|headers=hide|link=none|mainlabel=-|limit=500|searchlabel=}}')
if regAVTypeQuery then
for cell in regAVTypeQuery:gmatch('<td[^>]*>(.-)</td>') do
local deviceType = trim(cell:gsub('<[^>]+>', ''))
if deviceType and deviceType ~= '' then
registeredAVTypes[deviceType] = true
end
end
end
for deviceType, _ in pairs(usedAVTypes) do
if not registeredAVTypes[deviceType] then
missingAVTypes = missingAVTypes + 1
end
end
-- Build output
-- All category issues should be red/green (any missing = red)
local cards = {}
table.insert(cards, statusCard('CRT Brand Categories', missingCRTBrands, 'EveryCRT:Category maintenance#CRT Brands Missing Category Pages', 1, 1))
table.insert(cards, statusCard('CRT Type Categories', missingCRTTypes, 'EveryCRT:Category maintenance#CRT Types Missing Category Pages', 1, 1))
table.insert(cards, statusCard('AV Brand Categories', missingAVBrands, 'EveryCRT:Category maintenance#AV Brands Missing Category Pages', 1, 1))
table.insert(cards, statusCard('AV Type Categories', missingAVTypes, 'EveryCRT:Category maintenance#AV Device Types Missing Category Pages', 1, 1))
local output = '<div class="maintenance-status-grid">\n'
for _, card in ipairs(cards) do
output = output .. frame:preprocess(card) .. '\n'
end
output = output .. '</div>'
return output
end
-- Data quality status grid
function p.dataQualityStatus(frame)
local crtStubs = getCount(frame, '[[Category:CRT stubs]]')
local crtNoImage = getCount(frame, '[[Category:CRT models needing images]]')
local crtNoType = getCount(frame, '[[Category:CRTs missing type]]')
local crtNoSeries = getCount(frame, '[[Category:CRTs without series]]')
local avStubs = getCount(frame, '[[Category:AV device stubs]]')
local docsMissingContent = getCount(frame, '[[Category:Documents missing content]]')
local docsOrphaned = getCount(frame, '[[Category:Orphaned documents]]')
local missingBrandPages = countMissingBrandPages(frame)
local cards = {}
-- Missing brand pages: red/green (any = red) - links to section on this page
table.insert(cards, statusCard('Missing Brand Pages', missingBrandPages, 'EveryCRT:Maintenance#Missing Brand Pages', 1, 1))
-- Missing type: red/green (any missing = red)
table.insert(cards, statusCard('CRTs Missing Type', crtNoType, 'Category:CRTs missing type', 1, 1))
-- Stubs and missing images: yellow/green (warning at 1, no red)
table.insert(cards, statusCard('CRT Stubs', crtStubs, 'Category:CRT stubs', 1, 99999))
table.insert(cards, statusCard('CRTs Need Images', crtNoImage, 'Category:CRT models needing images', 1, 99999))
-- AV stubs: yellow/green (warning at 1, no red)
table.insert(cards, statusCard('AV Device Stubs', avStubs, 'Category:AV device stubs', 1, 99999))
-- Documents: yellow/green (warning at 1, no red)
table.insert(cards, statusCard('Docs Missing Content', docsMissingContent, 'Category:Documents missing content', 1, 99999))
table.insert(cards, statusCard('Orphaned Docs', docsOrphaned, 'Category:Orphaned documents', 1, 99999))
-- No series: informational only (gray), not an issue
table.insert(cards, statusCard('CRTs No Series', crtNoSeries, 'Category:CRTs without series', nil, nil, 'info'))
local output = '<div class="maintenance-status-grid">\n'
for _, card in ipairs(cards) do
output = output .. frame:preprocess(card) .. '\n'
end
output = output .. '</div>'
return output
end
-- Full dashboard combining all checks
function p.fullDashboard(frame)
local output = ''
output = output .. '=== Category System ===\n'
output = output .. p.categoryStatus(frame) .. '\n\n'
output = output .. '=== Data Quality ===\n'
output = output .. p.dataQualityStatus(frame) .. '\n'
return output
end
-- Summary counts for the main dashboard
function p.summaryCounts(frame)
-- Category issues
local categoryIssues = 0
-- Quick check for missing categories (simplified)
local usedBrands = {}
local brandQuery = frame:preprocess('{{#ask:[[Category:CRT models]][[Has brand::+]]|?Has brand|format=broadtable|headers=hide|link=none|mainlabel=-|limit=500|searchlabel=}}')
if brandQuery then
for cell in brandQuery:gmatch('<td[^>]*>(.-)</td>') do
local brand = trim(cell:gsub('<[^>]+>', ''))
if brand and brand ~= '' then usedBrands[brand] = true end
end
end
local registeredBrands = {}
local regQuery = frame:preprocess('{{#ask:[[Is CRT brand category::Yes]]|?Brand name|format=broadtable|headers=hide|link=none|mainlabel=-|limit=500|searchlabel=}}')
if regQuery then
for cell in regQuery:gmatch('<td[^>]*>(.-)</td>') do
local brand = trim(cell:gsub('<[^>]+>', ''))
if brand and brand ~= '' then registeredBrands[brand] = true end
end
end
for brand, _ in pairs(usedBrands) do
if not registeredBrands[brand] then categoryIssues = categoryIssues + 1 end
end
-- Data quality
local crtStubs = getCount(frame, '[[Category:CRT stubs]]')
local crtNoImage = getCount(frame, '[[Category:CRT models needing images]]')
-- Totals
local totalIssues = categoryIssues + crtStubs + crtNoImage
return tostring(totalIssues)
end
-- List missing brand pages for a detailed view
function p.listMissingBrandPages(frame)
local missingBrands = {}
local checkedBrands = {}
-- Get all brands from CRT models
local crtBrandQuery = frame:preprocess('{{#ask:[[Category:CRT models]][[Has brand::+]]|?Has brand|format=broadtable|headers=hide|link=none|mainlabel=-|limit=500|searchlabel=}}')
if crtBrandQuery then
for cell in crtBrandQuery:gmatch('<td[^>]*>(.-)</td>') do
local brand = trim(cell:gsub('<[^>]+>', ''))
if brand and brand ~= '' and not checkedBrands[brand] then
checkedBrands[brand] = true
local title = mw.title.new(brand)
if title and not title.exists then
table.insert(missingBrands, brand)
end
end
end
end
-- Get all brands from AV devices
local avBrandQuery = frame:preprocess('{{#ask:[[Category:AV devices]][[Has brand::+]]|?Has brand|format=broadtable|headers=hide|link=none|mainlabel=-|limit=500|searchlabel=}}')
if avBrandQuery then
for cell in avBrandQuery:gmatch('<td[^>]*>(.-)</td>') do
local brand = trim(cell:gsub('<[^>]+>', ''))
if brand and brand ~= '' and not checkedBrands[brand] then
checkedBrands[brand] = true
local title = mw.title.new(brand)
if title and not title.exists then
table.insert(missingBrands, brand)
end
end
end
end
table.sort(missingBrands)
if #missingBrands == 0 then
return "✓ ''All brand pages exist.''"
end
local output = {}
for _, brand in ipairs(missingBrands) do
-- Count how many products use this brand
local crtCount = getCount(frame, '[[Category:CRT models]][[Has brand::' .. brand .. ']]')
local avCount = getCount(frame, '[[Category:AV devices]][[Has brand::' .. brand .. ']]')
local totalCount = crtCount + avCount
table.insert(output, '* [[' .. brand .. ']] — ' .. totalCount .. ' products')
end
return table.concat(output, '\n')
end
return p