Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- palantir.lua
- -- Intelligence Dashboard for MtnLabs Server
- -- Run this in a multishell tab on the server
- -- Use global ndisk if available, otherwise load it
- local ndisk = _G.ndisk or dofile("ndisk_api.lua")
- if not _G.ndisk then
- ndisk.init()
- end
- -- ============================================
- -- CONFIGURATION
- -- ============================================
- local GRID_SIZE = 200 -- Block size for location clustering
- local ANOMALY_MULTIPLIER = 2 -- Flag transactions > Nx average
- local ADMIN_ACCOUNT = "admin" -- Master admin, always has access
- local ACCESS_FILE = "palantir_access.txt" -- Stored on NDisk
- -- ============================================
- -- MODEM FOR SERVER VERIFICATION
- -- ============================================
- local modem = peripheral.find("modem")
- if modem then modem.open(1) end
- -- Load NDisk secret for direct node queries
- local NDISK_SECRET = nil
- if fs.exists(".ndisk_secret") then
- local f = fs.open(".ndisk_secret", "r")
- NDISK_SECRET = f.readAll()
- f.close()
- end
- local function sendServerRequest(request, expectedType)
- if not modem then return nil end
- modem.transmit(1, 1, request)
- local timer = os.startTimer(5)
- while true do
- local event, param1, _, _, message = os.pullEvent()
- if event == "modem_message" and type(message) == "table" then
- if expectedType then
- if message.type == expectedType then return message end
- if message.type == "error" then return nil end
- else
- if message.type then return message end
- end
- elseif event == "timer" and param1 == timer then
- return nil
- end
- end
- end
- -- ============================================
- -- ACCESS CONTROL
- -- ============================================
- local currentUser = nil
- local function loadAccessList()
- local data = ndisk.read(ACCESS_FILE)
- if data then
- return textutils.unserialize(data) or {}
- end
- return {}
- end
- local function saveAccessList(list)
- ndisk.write(ACCESS_FILE, textutils.serialize(list))
- end
- local function hasAccess(studentId)
- if studentId == ADMIN_ACCOUNT then return true end
- local list = loadAccessList()
- for _, id in ipairs(list) do
- if id == studentId then return true end
- end
- return false
- end
- local function grantAccess(studentId)
- if studentId == ADMIN_ACCOUNT then return true end
- local list = loadAccessList()
- for _, id in ipairs(list) do
- if id == studentId then return true end -- Already has access
- end
- table.insert(list, studentId)
- saveAccessList(list)
- return true
- end
- local function revokeAccess(studentId)
- if studentId == ADMIN_ACCOUNT then return false end -- Can't revoke admin
- local list = loadAccessList()
- local newList = {}
- local found = false
- for _, id in ipairs(list) do
- if id ~= studentId then
- table.insert(newList, id)
- else
- found = true
- end
- end
- if found then
- saveAccessList(newList)
- end
- return found
- end
- local function verifyLogin(studentId, password)
- -- Direct NDisk verification (we're on the server)
- local passPath = "students/" .. studentId .. "/password.txt"
- local storedPass = ndisk.read(passPath)
- if storedPass then
- -- Trim whitespace
- storedPass = storedPass:gsub("^%s*(.-)%s*$", "%1")
- return storedPass == password
- end
- return false
- end
- -- ============================================
- -- UTILITY FUNCTIONS
- -- ============================================
- local function clear()
- term.clear()
- term.setCursorPos(1, 1)
- end
- local function centerText(text, y)
- local w, _ = term.getSize()
- term.setCursorPos(math.floor((w - #text) / 2) + 1, y)
- term.write(text)
- end
- local function drawHeader(title)
- clear()
- local w, _ = term.getSize()
- term.setTextColor(colors.cyan)
- print(string.rep("=", w))
- centerText("P A L A N T I R", 2)
- term.setCursorPos(1, 3)
- print(string.rep("=", w))
- term.setTextColor(colors.yellow)
- centerText(title, 4)
- term.setTextColor(colors.white)
- print("")
- print("")
- end
- local function prompt(msg)
- term.setTextColor(colors.gray)
- write(msg or "> ")
- term.setTextColor(colors.white)
- return read()
- end
- local function pause()
- term.setTextColor(colors.gray)
- print("")
- print("Press any key to continue...")
- os.pullEvent("key")
- end
- local function getStudentList()
- local students = ndisk.list("students")
- return students or {}
- end
- -- ============================================
- -- APP 1: NDISK TERMINAL (inline version)
- -- ============================================
- local function appNDiskTerminal()
- drawHeader("NDisk Terminal")
- print("Commands: list, read, write, delete, space, help, exit")
- print("")
- while true do
- local input = prompt("ndisk> ")
- local command = {}
- for word in input:gmatch("%S+") do
- table.insert(command, word)
- end
- local cmd = command[1]
- local arg1 = command[2]
- if cmd == "list" then
- local path = arg1 or ""
- local items = ndisk.list(path)
- print("Listing '" .. path .. "':")
- if #items == 0 then
- print(" (No files found)")
- else
- for _, item in ipairs(items) do
- print(" " .. item)
- end
- end
- elseif cmd == "read" and arg1 then
- local content, err = ndisk.read(arg1)
- if content then
- print("--- BEGIN ---")
- print(content)
- print("--- END ---")
- else
- print("Error: " .. (err or "Unknown"))
- end
- elseif cmd == "write" and arg1 then
- print("Enter content (type EOF to finish):")
- local content = ""
- while true do
- local line = read()
- if line == "EOF" then break end
- content = content .. line .. "\n"
- end
- local success, err = ndisk.write(arg1, content)
- print(success and "Written." or ("Error: " .. (err or "Unknown")))
- elseif cmd == "delete" and arg1 then
- local success, err = ndisk.delete(arg1)
- print(success and "Deleted." or ("Error: " .. (err or "Unknown")))
- elseif cmd == "space" then
- local space = ndisk.getFreeSpace()
- print("Free: " .. math.floor(space/1024) .. " KB")
- elseif cmd == "help" then
- print("list [path], read <path>, write <path>,")
- print("delete <path>, space, exit")
- elseif cmd == "exit" or cmd == "q" then
- break
- elseif cmd and #cmd > 0 then
- print("Unknown command. Type 'help'.")
- end
- end
- end
- -- ============================================
- -- APP 2: EMAIL SCRAPER
- -- ============================================
- local function loadAllEmails()
- local allEmails = {}
- local students = getStudentList()
- for _, studentId in ipairs(students) do
- local emailPath = "students/" .. studentId .. "/emails.txt"
- local data = ndisk.read(emailPath)
- if data then
- local emails = textutils.unserialize(data)
- if emails then
- for _, email in ipairs(emails) do
- email.recipient = studentId
- table.insert(allEmails, email)
- end
- end
- end
- end
- -- Sort by timestamp (newest first)
- table.sort(allEmails, function(a, b)
- return (a.timestamp or 0) > (b.timestamp or 0)
- end)
- return allEmails
- end
- local function displayEmail(email, index)
- term.setTextColor(colors.yellow)
- print("[" .. (index or "?") .. "] " .. (email.sender or "?") .. " -> " .. (email.recipient or "?"))
- term.setTextColor(colors.white)
- print(" Subject: " .. (email.subject or "(no subject)"))
- term.setTextColor(colors.lightGray)
- local body = email.body or ""
- if #body > 60 then body = body:sub(1, 57) .. "..." end
- print(" " .. body)
- term.setTextColor(colors.white)
- end
- local function appEmailScraper()
- drawHeader("Email Scraper")
- print("Commands:")
- print(" latest [n] - Show latest n emails (default 5)")
- print(" student <id> - All emails to/from student")
- print(" search <word> - Search all emails for keyword")
- print(" read <n> - Read full email by index")
- print(" exit - Return to main menu")
- print("")
- local cachedEmails = {}
- while true do
- local input = prompt("email> ")
- local parts = {}
- for word in input:gmatch("%S+") do
- table.insert(parts, word)
- end
- local cmd = parts[1]
- local arg = parts[2]
- if cmd == "latest" then
- local count = tonumber(arg) or 5
- print("Loading emails...")
- cachedEmails = loadAllEmails()
- print("")
- term.setTextColor(colors.cyan)
- print("LATEST " .. math.min(count, #cachedEmails) .. " EMAILS:")
- term.setTextColor(colors.white)
- print("")
- for i = 1, math.min(count, #cachedEmails) do
- displayEmail(cachedEmails[i], i)
- print("")
- end
- elseif cmd == "student" and arg then
- print("Loading emails for " .. arg .. "...")
- local allEmails = loadAllEmails()
- cachedEmails = {}
- for _, email in ipairs(allEmails) do
- if email.sender == arg or email.recipient == arg then
- table.insert(cachedEmails, email)
- end
- end
- print("")
- term.setTextColor(colors.cyan)
- print("EMAILS FOR: " .. arg .. " (" .. #cachedEmails .. " found)")
- term.setTextColor(colors.white)
- print("")
- for i, email in ipairs(cachedEmails) do
- displayEmail(email, i)
- print("")
- if i >= 10 then
- print("... and " .. (#cachedEmails - 10) .. " more")
- break
- end
- end
- elseif cmd == "search" and arg then
- local keyword = table.concat(parts, " ", 2):lower()
- print("Searching for '" .. keyword .. "'...")
- local allEmails = loadAllEmails()
- cachedEmails = {}
- for _, email in ipairs(allEmails) do
- local haystack = ((email.subject or "") .. " " .. (email.body or "")):lower()
- if haystack:find(keyword, 1, true) then
- table.insert(cachedEmails, email)
- end
- end
- print("")
- term.setTextColor(colors.cyan)
- print("SEARCH: '" .. keyword .. "' (" .. #cachedEmails .. " matches)")
- term.setTextColor(colors.white)
- print("")
- for i, email in ipairs(cachedEmails) do
- displayEmail(email, i)
- print("")
- if i >= 10 then
- print("... and " .. (#cachedEmails - 10) .. " more")
- break
- end
- end
- elseif cmd == "read" and arg then
- local idx = tonumber(arg)
- if idx and cachedEmails[idx] then
- local email = cachedEmails[idx]
- print("")
- term.setTextColor(colors.yellow)
- print("From: " .. (email.sender or "?"))
- print("To: " .. (email.recipient or "?"))
- print("Subject: " .. (email.subject or "(none)"))
- term.setTextColor(colors.white)
- print(string.rep("-", 40))
- print(email.body or "(empty)")
- print(string.rep("-", 40))
- else
- print("Invalid index. Run a search first.")
- end
- elseif cmd == "exit" or cmd == "q" then
- break
- elseif cmd and #cmd > 0 then
- print("Unknown. Try: latest, student, search, read, exit")
- end
- end
- end
- -- ============================================
- -- APP 3: LOCATION INTEL
- -- ============================================
- local function parseLocationHistory(studentId)
- local historyPath = "students/" .. studentId .. "/location_history.txt"
- local data = ndisk.read(historyPath)
- if not data then return {} end
- local locations = {}
- for line in data:gmatch("[^\n]+") do
- local timestamp, coords = line:match("([^|]+)|(.+)")
- if coords then
- local x, y, z = coords:match("([%-?%d]+),([%-?%d]+),([%-?%d]+)")
- if x and y and z then
- table.insert(locations, {
- timestamp = tonumber(timestamp) or 0,
- x = tonumber(x),
- y = tonumber(y),
- z = tonumber(z)
- })
- end
- end
- end
- return locations
- end
- local function clusterLocations(locations)
- local clusters = {} -- key = "gridX,gridZ" -> count
- for _, loc in ipairs(locations) do
- local gridX = math.floor(loc.x / GRID_SIZE) * GRID_SIZE
- local gridZ = math.floor(loc.z / GRID_SIZE) * GRID_SIZE
- local key = gridX .. "," .. gridZ
- clusters[key] = (clusters[key] or 0) + 1
- end
- -- Convert to sorted list
- local hotspots = {}
- for key, count in pairs(clusters) do
- local gx, gz = key:match("([%-?%d]+),([%-?%d]+)")
- table.insert(hotspots, {
- x = tonumber(gx) + math.floor(GRID_SIZE/2), -- Center of grid
- z = tonumber(gz) + math.floor(GRID_SIZE/2),
- visits = count
- })
- end
- table.sort(hotspots, function(a, b) return a.visits > b.visits end)
- return hotspots
- end
- local function appLocationIntel()
- drawHeader("Location Intel")
- print("Commands:")
- print(" list - Show all tracked students")
- print(" analyze <id> - Show hotspots for student")
- print(" track <id> - Show last known location")
- print(" region <x> <z> - Who visits this area?")
- print(" exit - Return to main menu")
- print("")
- while true do
- local input = prompt("loc> ")
- local parts = {}
- for word in input:gmatch("%S+") do
- table.insert(parts, word)
- end
- local cmd = parts[1]
- local arg1 = parts[2]
- local arg2 = parts[3]
- if cmd == "list" then
- local students = getStudentList()
- print("")
- term.setTextColor(colors.cyan)
- print("TRACKED STUDENTS (" .. #students .. "):")
- term.setTextColor(colors.white)
- for i, s in ipairs(students) do
- write(s .. " ")
- if i % 5 == 0 then print("") end
- end
- print("")
- elseif cmd == "analyze" and arg1 then
- print("Analyzing " .. arg1 .. "...")
- local locations = parseLocationHistory(arg1)
- if #locations == 0 then
- print("No location data for " .. arg1)
- else
- local hotspots = clusterLocations(locations)
- print("")
- term.setTextColor(colors.cyan)
- print(arg1:upper() .. "'s HOTSPOTS (" .. #locations .. " total pings):")
- term.setTextColor(colors.white)
- print("")
- for i = 1, math.min(10, #hotspots) do
- local h = hotspots[i]
- local label = ""
- if i == 1 then label = " <- likely base" end
- if h.visits > 20 then label = label .. " [FREQUENT]" end
- print(string.format(" #%d: %d, *, %d (%d visits)%s",
- i, h.x, h.z, h.visits, label))
- end
- end
- print("")
- elseif cmd == "track" and arg1 then
- local locations = parseLocationHistory(arg1)
- if #locations == 0 then
- print("No location data for " .. arg1)
- else
- -- Most recent is last in list (chronological) or first if sorted
- local latest = locations[#locations]
- local age = math.floor((os.epoch("local") - latest.timestamp) / 60000)
- print("")
- term.setTextColor(colors.yellow)
- print(arg1:upper() .. " LAST SEEN:")
- term.setTextColor(colors.white)
- print(string.format(" Location: %d, %d, %d", latest.x, latest.y, latest.z))
- print(string.format(" Time: %d minutes ago", age))
- end
- print("")
- elseif cmd == "region" and arg1 and arg2 then
- local targetX = tonumber(arg1)
- local targetZ = tonumber(arg2)
- if not targetX or not targetZ then
- print("Usage: region <x> <z>")
- else
- local gridX = math.floor(targetX / GRID_SIZE) * GRID_SIZE
- local gridZ = math.floor(targetZ / GRID_SIZE) * GRID_SIZE
- print("Scanning region around " .. gridX .. ", " .. gridZ .. "...")
- local visitors = {}
- local students = getStudentList()
- for _, studentId in ipairs(students) do
- local locations = parseLocationHistory(studentId)
- local count = 0
- for _, loc in ipairs(locations) do
- local lx = math.floor(loc.x / GRID_SIZE) * GRID_SIZE
- local lz = math.floor(loc.z / GRID_SIZE) * GRID_SIZE
- if lx == gridX and lz == gridZ then
- count = count + 1
- end
- end
- if count > 0 then
- table.insert(visitors, {id = studentId, visits = count})
- end
- end
- table.sort(visitors, function(a, b) return a.visits > b.visits end)
- print("")
- term.setTextColor(colors.cyan)
- print("VISITORS TO REGION " .. gridX .. ", " .. gridZ .. ":")
- term.setTextColor(colors.white)
- if #visitors == 0 then
- print(" No one has been here")
- else
- for _, v in ipairs(visitors) do
- print(" " .. v.id .. ": " .. v.visits .. " visits")
- end
- end
- end
- print("")
- elseif cmd == "exit" or cmd == "q" then
- break
- elseif cmd and #cmd > 0 then
- print("Unknown. Try: list, analyze, track, region, exit")
- end
- end
- end
- -- ============================================
- -- APP 4: TRANSACTION MONITOR
- -- ============================================
- local function loadTransactions()
- local txPath = "transactions/log.txt"
- local data = ndisk.read(txPath)
- if not data then return {} end
- local transactions = {}
- for line in data:gmatch("[^\n]+") do
- local timestamp, studentId, action, amount, balance =
- line:match("([^|]+)|([^|]+)|([^|]+)|([^|]+)|([^|]*)")
- if timestamp and studentId then
- table.insert(transactions, {
- timestamp = tonumber(timestamp) or 0,
- student = studentId,
- action = action,
- amount = tonumber(amount) or 0,
- balance = tonumber(balance) or 0
- })
- end
- end
- return transactions
- end
- local function appTransactionMonitor()
- drawHeader("Transaction Monitor")
- print("Commands:")
- print(" recent [n] - Show last n transactions")
- print(" stats - Show transaction statistics")
- print(" suspicious - Show flagged transactions")
- print(" student <id> - Transactions for student")
- print(" exit - Return to main menu")
- print("")
- while true do
- local input = prompt("tx> ")
- local parts = {}
- for word in input:gmatch("%S+") do
- table.insert(parts, word)
- end
- local cmd = parts[1]
- local arg = parts[2]
- if cmd == "recent" then
- local count = tonumber(arg) or 10
- local txs = loadTransactions()
- -- Filter out admin
- local filtered = {}
- for i = #txs, 1, -1 do -- Reverse for newest first
- if txs[i].student ~= ADMIN_ACCOUNT then
- table.insert(filtered, txs[i])
- if #filtered >= count then break end
- end
- end
- print("")
- term.setTextColor(colors.cyan)
- print("RECENT TRANSACTIONS:")
- term.setTextColor(colors.white)
- for _, tx in ipairs(filtered) do
- local age = math.floor((os.epoch("local") - tx.timestamp) / 60000)
- local sign = tx.action == "deposit" and "+" or "-"
- print(string.format(" %s %s%d E (%dm ago) bal:%d",
- tx.student, sign, tx.amount, age, tx.balance))
- end
- print("")
- elseif cmd == "stats" then
- local txs = loadTransactions()
- local total = 0
- local count = 0
- local deposits = 0
- local withdrawals = 0
- for _, tx in ipairs(txs) do
- if tx.student ~= ADMIN_ACCOUNT then
- total = total + tx.amount
- count = count + 1
- if tx.action == "deposit" then
- deposits = deposits + tx.amount
- else
- withdrawals = withdrawals + tx.amount
- end
- end
- end
- local avg = count > 0 and (total / count) or 0
- print("")
- term.setTextColor(colors.cyan)
- print("TRANSACTION STATISTICS:")
- term.setTextColor(colors.white)
- print(" Total transactions: " .. count)
- print(" Total volume: " .. total .. " E")
- print(" Average amount: " .. string.format("%.1f", avg) .. " E")
- print(" Total deposits: " .. deposits .. " E")
- print(" Total withdrawals: " .. withdrawals .. " E")
- print(" Anomaly threshold: >" .. string.format("%.1f", avg * ANOMALY_MULTIPLIER) .. " E")
- print("")
- elseif cmd == "suspicious" then
- local txs = loadTransactions()
- local total = 0
- local count = 0
- -- Calculate average first
- for _, tx in ipairs(txs) do
- if tx.student ~= ADMIN_ACCOUNT then
- total = total + tx.amount
- count = count + 1
- end
- end
- local avg = count > 0 and (total / count) or 0
- local threshold = avg * ANOMALY_MULTIPLIER
- if threshold < 20 then threshold = 20 end -- Minimum threshold
- -- Find suspicious
- local suspicious = {}
- for i = #txs, 1, -1 do
- local tx = txs[i]
- if tx.student ~= ADMIN_ACCOUNT and tx.amount > threshold then
- table.insert(suspicious, tx)
- if #suspicious >= 20 then break end
- end
- end
- print("")
- term.setTextColor(colors.red)
- print("SUSPICIOUS TRANSACTIONS (>" .. string.format("%.0f", threshold) .. " E):")
- term.setTextColor(colors.white)
- if #suspicious == 0 then
- print(" None found")
- else
- for _, tx in ipairs(suspicious) do
- local age = math.floor((os.epoch("local") - tx.timestamp) / 60000)
- local sign = tx.action == "deposit" and "+" or "-"
- term.setTextColor(colors.orange)
- print(string.format(" [!] %s %s%d E (%dm ago)",
- tx.student, sign, tx.amount, age))
- end
- end
- term.setTextColor(colors.white)
- print("")
- elseif cmd == "student" and arg then
- local txs = loadTransactions()
- local filtered = {}
- for i = #txs, 1, -1 do
- if txs[i].student == arg then
- table.insert(filtered, txs[i])
- if #filtered >= 20 then break end
- end
- end
- print("")
- term.setTextColor(colors.cyan)
- print("TRANSACTIONS FOR: " .. arg)
- term.setTextColor(colors.white)
- if #filtered == 0 then
- print(" No transactions found")
- else
- for _, tx in ipairs(filtered) do
- local age = math.floor((os.epoch("local") - tx.timestamp) / 60000)
- local sign = tx.action == "deposit" and "+" or "-"
- print(string.format(" %s%d E (%dm ago) bal:%d",
- sign, tx.amount, age, tx.balance))
- end
- end
- print("")
- elseif cmd == "exit" or cmd == "q" then
- break
- elseif cmd and #cmd > 0 then
- print("Unknown. Try: recent, stats, suspicious, student, exit")
- end
- end
- end
- -- ============================================
- -- APP 5: RAID STATUS DASHBOARD
- -- ============================================
- local function checkNodeHealth(nodeId)
- -- Try to ping the node via getSpace request
- local modemNdisk = peripheral.find("modem")
- if not modemNdisk then return false end
- modemNdisk.open(5)
- local requestId = os.getComputerID() .. "_health_" .. os.epoch("utc") .. "_" .. math.random(10000)
- modemNdisk.transmit(5, 5, {
- targetNode = nodeId,
- id = requestId,
- action = "getSpace",
- secret = NDISK_SECRET -- Include auth secret
- })
- local timer = os.startTimer(2)
- while true do
- local event, p1, p2, p3, p4 = os.pullEvent()
- if event == "modem_message" and type(p4) == "table" and p4.id == requestId then
- return p4.response and p4.response.success, p4.response and p4.response.space or 0
- elseif event == "timer" and p1 == timer then
- return false, 0
- end
- end
- end
- local function appRaidStatus()
- drawHeader("RAID Status Dashboard")
- print("Commands:")
- print(" status - Show overall RAID health")
- print(" nodes - Check all node health")
- print(" files - Show file redundancy status")
- print(" refresh - Refresh node discovery")
- print(" exit - Return to main menu")
- print("")
- while true do
- local input = prompt("raid> ")
- local parts = {}
- for word in input:gmatch("%S+") do
- table.insert(parts, word)
- end
- local cmd = parts[1]
- if cmd == "status" then
- print("")
- local raidStatus = ndisk.getRaidStatus()
- term.setTextColor(colors.cyan)
- print("RAID CLUSTER STATUS:")
- term.setTextColor(colors.white)
- print("")
- -- Mode
- if raidStatus.enabled then
- term.setTextColor(colors.lime)
- print(" Mode: RAID10 (Mirrored)")
- else
- term.setTextColor(colors.red)
- print(" Mode: DEGRADED (Single-copy)")
- end
- term.setTextColor(colors.white)
- print(" Total Nodes: " .. raidStatus.nodeCount)
- print(" Max Failures: " .. raidStatus.maxFailures .. " nodes can go down")
- -- Free space
- local freeSpace = ndisk.getFreeSpace()
- print(" Usable Space: " .. math.floor(freeSpace / 1024) .. " KB")
- print("")
- elseif cmd == "nodes" then
- print("")
- print("Checking node health...")
- print("")
- -- Load node IDs from ids.txt
- local nodeIds = {}
- if fs.exists("ids.txt") then
- local file = fs.open("ids.txt", "r")
- for line in file.readLine do
- local id, num = line:match("(%d+)%s+(%d+)")
- if id and num then
- nodeIds[tonumber(num)] = tonumber(id)
- end
- end
- file.close()
- end
- local healthy = 0
- local unhealthy = 0
- local sortedNums = {}
- for num, _ in pairs(nodeIds) do
- table.insert(sortedNums, num)
- end
- table.sort(sortedNums)
- term.setTextColor(colors.cyan)
- print("NODE HEALTH:")
- term.setTextColor(colors.white)
- for _, num in ipairs(sortedNums) do
- local nodeId = nodeIds[num]
- local isHealthy, space = checkNodeHealth(nodeId)
- if isHealthy then
- healthy = healthy + 1
- term.setTextColor(colors.lime)
- print(" [" .. num .. "] Node " .. nodeId .. ": OK (" .. math.floor(space/1024) .. " KB free)")
- else
- unhealthy = unhealthy + 1
- term.setTextColor(colors.red)
- print(" [" .. num .. "] Node " .. nodeId .. ": OFFLINE")
- end
- end
- term.setTextColor(colors.white)
- print("")
- print("Summary: " .. healthy .. " healthy, " .. unhealthy .. " offline")
- local raidStatus = ndisk.getRaidStatus()
- if unhealthy > 0 and unhealthy <= raidStatus.maxFailures then
- term.setTextColor(colors.yellow)
- print("WARNING: Running degraded but still functional")
- elseif unhealthy > raidStatus.maxFailures then
- term.setTextColor(colors.red)
- print("CRITICAL: Data at risk! Too many nodes offline!")
- else
- term.setTextColor(colors.lime)
- print("All nodes healthy - full redundancy active")
- end
- term.setTextColor(colors.white)
- print("")
- elseif cmd == "files" then
- print("")
- print("Analyzing file redundancy...")
- -- Load chunk map and check RAID status per file
- local fileStats = {raid10 = 0, single = 0, total = 0}
- if fs.exists("chunklocation.txt") then
- local file = fs.open("chunklocation.txt", "r")
- local files = {}
- for line in file.readLine do
- local parts = {}
- for part in line:gmatch("%S+") do
- table.insert(parts, part)
- end
- if #parts >= 3 then
- local filename = parts[1]
- local hasRaid = #parts >= 4 and parts[3] ~= parts[4]
- if not files[filename] then
- files[filename] = {hasRaid = true}
- fileStats.total = fileStats.total + 1
- end
- if not hasRaid then
- files[filename].hasRaid = false
- end
- end
- end
- file.close()
- for _, info in pairs(files) do
- if info.hasRaid then
- fileStats.raid10 = fileStats.raid10 + 1
- else
- fileStats.single = fileStats.single + 1
- end
- end
- end
- print("")
- term.setTextColor(colors.cyan)
- print("FILE REDUNDANCY STATUS:")
- term.setTextColor(colors.white)
- print(" Total files: " .. fileStats.total)
- term.setTextColor(colors.lime)
- print(" RAID10 protected: " .. fileStats.raid10)
- if fileStats.single > 0 then
- term.setTextColor(colors.yellow)
- print(" Single-copy (at risk): " .. fileStats.single)
- print("")
- print(" Run ndisk_migrate_raid10 to protect these files")
- else
- print("")
- term.setTextColor(colors.lime)
- print(" All files have RAID10 protection!")
- end
- term.setTextColor(colors.white)
- print("")
- elseif cmd == "refresh" then
- print("")
- print("Refreshing node discovery...")
- ndisk.refreshNodes()
- local raidStatus = ndisk.getRaidStatus()
- term.setTextColor(colors.lime)
- print("Found " .. raidStatus.nodeCount .. " nodes")
- term.setTextColor(colors.white)
- print("")
- elseif cmd == "exit" or cmd == "q" then
- break
- elseif cmd and #cmd > 0 then
- print("Unknown. Try: status, nodes, files, refresh, exit")
- end
- end
- end
- -- ============================================
- -- MAIN MENU
- -- ============================================
- local function appAccessManagement()
- if currentUser ~= ADMIN_ACCOUNT then
- print("Access denied. Admin only.")
- pause()
- return
- end
- drawHeader("Access Management")
- print("Commands:")
- print(" list - Show authorized users")
- print(" grant <id> - Grant access to user")
- print(" revoke <id> - Revoke access from user")
- print(" exit - Return to main menu")
- print("")
- while true do
- local input = prompt("access> ")
- local parts = {}
- for word in input:gmatch("%S+") do
- table.insert(parts, word)
- end
- local cmd = parts[1]
- local arg = parts[2]
- if cmd == "list" then
- local list = loadAccessList()
- print("")
- term.setTextColor(colors.cyan)
- print("AUTHORIZED USERS:")
- term.setTextColor(colors.yellow)
- print(" " .. ADMIN_ACCOUNT .. " (master)")
- term.setTextColor(colors.white)
- for _, id in ipairs(list) do
- print(" " .. id)
- end
- if #list == 0 then
- print(" (no additional users)")
- end
- print("")
- elseif cmd == "grant" and arg then
- if grantAccess(arg) then
- term.setTextColor(colors.lime)
- print("Access granted to: " .. arg)
- term.setTextColor(colors.white)
- end
- elseif cmd == "revoke" and arg then
- if revokeAccess(arg) then
- term.setTextColor(colors.orange)
- print("Access revoked from: " .. arg)
- term.setTextColor(colors.white)
- else
- print("User not found or cannot be revoked.")
- end
- elseif cmd == "exit" or cmd == "q" then
- break
- elseif cmd and #cmd > 0 then
- print("Unknown. Try: list, grant, revoke, exit")
- end
- end
- end
- -- ============================================
- -- APP 6: RAID STATUS
- -- ============================================
- local function appRaidStatus()
- drawHeader("RAID Status")
- -- Load repair library
- local repair = dofile("ndisk_repair.lua")
- print("Checking node health...")
- print("")
- -- Get node health
- local health = repair.getNodeHealth()
- local onlineCount = 0
- local totalCount = 0
- -- Load node IDs for proper ordering
- local nodeOrder = {}
- if fs.exists("ids.txt") then
- local file = fs.open("ids.txt", "r")
- for line in file.readLine do
- local nodeId, hierarchicalNum = line:match("(%d+)%s+(%d+)")
- if nodeId and hierarchicalNum then
- table.insert(nodeOrder, {id = tonumber(nodeId), idx = tonumber(hierarchicalNum)})
- end
- end
- file.close()
- table.sort(nodeOrder, function(a, b) return a.idx < b.idx end)
- end
- -- Display node status
- term.setTextColor(colors.cyan)
- print("NODE STATUS:")
- term.setTextColor(colors.white)
- for _, node in ipairs(nodeOrder) do
- totalCount = totalCount + 1
- local info = health[node.id] or {online = false}
- if info.online then
- onlineCount = onlineCount + 1
- term.setTextColor(colors.lime)
- print(" Node " .. node.idx .. " (ID:" .. node.id .. "): OK")
- else
- term.setTextColor(colors.red)
- print(" Node " .. node.idx .. " (ID:" .. node.id .. "): DOWN")
- end
- end
- term.setTextColor(colors.white)
- print("")
- print("Online: " .. onlineCount .. "/" .. totalCount)
- -- Check degraded chunks
- local degradedCount = repair.getDegradedCount()
- if degradedCount > 0 then
- term.setTextColor(colors.orange)
- print("Degraded chunks: " .. degradedCount)
- term.setTextColor(colors.white)
- else
- term.setTextColor(colors.lime)
- print("All chunks healthy (no degraded)")
- term.setTextColor(colors.white)
- end
- print("")
- print("Commands: repair, refresh, exit")
- print("")
- while true do
- local input = prompt("raid> ")
- if input == "repair" or input == "r" then
- print("")
- term.setTextColor(colors.yellow)
- print("Starting repair (server will enter maintenance mode)...")
- term.setTextColor(colors.white)
- -- Send maintenance start
- if modem then
- modem.transmit(1, 1, {type = "maintenance_start"})
- sleep(0.5)
- end
- -- Run repair
- print("Repairing...")
- local stats = repair.run()
- -- Send maintenance end
- if modem then
- modem.transmit(1, 1, {type = "maintenance_end"})
- end
- print("")
- term.setTextColor(colors.lime)
- print("Repair complete!")
- print(" Files repaired: " .. stats.filesRepaired)
- if stats.errors > 0 then
- term.setTextColor(colors.red)
- print(" Errors: " .. stats.errors)
- for _, err in ipairs(stats.errorList) do
- print(" " .. err)
- end
- end
- term.setTextColor(colors.white)
- print("")
- elseif input == "refresh" then
- return appRaidStatus() -- Restart the function
- elseif input == "exit" or input == "q" then
- break
- elseif input and #input > 0 then
- print("Commands: repair, refresh, exit")
- end
- end
- end
- local function drawMainMenu()
- clear()
- local w, _ = term.getSize()
- term.setTextColor(colors.cyan)
- print(string.rep("=", w))
- print("")
- centerText("P A L A N T I R", 3)
- term.setTextColor(colors.lightGray)
- centerText("Intelligence Dashboard", 4)
- term.setTextColor(colors.cyan)
- print("")
- term.setCursorPos(1, 6)
- print(string.rep("=", w))
- term.setTextColor(colors.white)
- print("")
- -- Show logged in user
- term.setTextColor(colors.gray)
- print(" Logged in as: " .. (currentUser or "unknown"))
- -- Count some stats
- local students = getStudentList()
- print(" Students tracked: " .. #students)
- print("")
- term.setTextColor(colors.white)
- print(" [1] NDisk Terminal")
- print(" [2] Email Scraper")
- print(" [3] Location Intel")
- print(" [4] Transaction Monitor")
- print(" [5] RAID Status")
- -- Admin-only option
- if currentUser == ADMIN_ACCOUNT then
- term.setTextColor(colors.yellow)
- print(" [6] Access Management")
- end
- print("")
- term.setTextColor(colors.red)
- print(" [Q] Logout")
- term.setTextColor(colors.white)
- print("")
- end
- local function loginScreen()
- clear()
- local w, _ = term.getSize()
- term.setTextColor(colors.cyan)
- print(string.rep("=", w))
- centerText("P A L A N T I R", 2)
- term.setCursorPos(1, 3)
- print(string.rep("=", w))
- term.setTextColor(colors.red)
- print("")
- centerText("AUTHORIZED ACCESS ONLY", 5)
- term.setTextColor(colors.white)
- print("")
- print("")
- write(" Username: ")
- local username = read()
- write(" Password: ")
- local password = read("*")
- print("")
- print(" Verifying with server...")
- -- Verify with server
- if not verifyLogin(username, password) then
- term.setTextColor(colors.red)
- print(" Login failed!")
- term.setTextColor(colors.white)
- sleep(2)
- return nil
- end
- -- Check access list
- if not hasAccess(username) then
- term.setTextColor(colors.red)
- print(" Access denied. You are not authorized.")
- term.setTextColor(colors.white)
- sleep(2)
- return nil
- end
- term.setTextColor(colors.lime)
- print(" Access granted.")
- term.setTextColor(colors.white)
- sleep(1)
- return username
- end
- local function mainLoop()
- while true do
- drawMainMenu()
- local input = prompt("Select: ")
- if input == "1" then
- appNDiskTerminal()
- elseif input == "2" then
- appEmailScraper()
- elseif input == "3" then
- appLocationIntel()
- elseif input == "4" then
- appTransactionMonitor()
- elseif input == "5" then
- appRaidStatus()
- elseif input == "6" and currentUser == ADMIN_ACCOUNT then
- appAccessManagement()
- elseif input:lower() == "q" or input:lower() == "logout" then
- clear()
- print("Logged out.")
- currentUser = nil
- break
- end
- end
- end
- local function main()
- -- Keep running forever, return to login on logout
- while true do
- currentUser = loginScreen()
- if currentUser then
- mainLoop()
- end
- end
- end
- -- Run
- main()
Advertisement