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
MediaWiki Appliance - Powered by TurnKey Linux