MtnMCG

palantir.lua

Dec 20th, 2025 (edited)
78
0
Never
1
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 42.58 KB | None | 0 0
  1. -- palantir.lua
  2. -- Intelligence Dashboard for MtnLabs Server
  3. -- Run this in a multishell tab on the server
  4.  
  5. -- Use global ndisk if available, otherwise load it
  6. local ndisk = _G.ndisk or dofile("ndisk_api.lua")
  7. if not _G.ndisk then
  8. ndisk.init()
  9. end
  10.  
  11. -- ============================================
  12. -- CONFIGURATION
  13. -- ============================================
  14. local GRID_SIZE = 200 -- Block size for location clustering
  15. local ANOMALY_MULTIPLIER = 2 -- Flag transactions > Nx average
  16. local ADMIN_ACCOUNT = "admin" -- Master admin, always has access
  17. local ACCESS_FILE = "palantir_access.txt" -- Stored on NDisk
  18.  
  19. -- ============================================
  20. -- MODEM FOR SERVER VERIFICATION
  21. -- ============================================
  22. local modem = peripheral.find("modem")
  23. if modem then modem.open(1) end
  24.  
  25. -- Load NDisk secret for direct node queries
  26. local NDISK_SECRET = nil
  27. if fs.exists(".ndisk_secret") then
  28. local f = fs.open(".ndisk_secret", "r")
  29. NDISK_SECRET = f.readAll()
  30. f.close()
  31. end
  32.  
  33. local function sendServerRequest(request, expectedType)
  34. if not modem then return nil end
  35. modem.transmit(1, 1, request)
  36. local timer = os.startTimer(5)
  37. while true do
  38. local event, param1, _, _, message = os.pullEvent()
  39. if event == "modem_message" and type(message) == "table" then
  40. if expectedType then
  41. if message.type == expectedType then return message end
  42. if message.type == "error" then return nil end
  43. else
  44. if message.type then return message end
  45. end
  46. elseif event == "timer" and param1 == timer then
  47. return nil
  48. end
  49. end
  50. end
  51.  
  52. -- ============================================
  53. -- ACCESS CONTROL
  54. -- ============================================
  55. local currentUser = nil
  56.  
  57. local function loadAccessList()
  58. local data = ndisk.read(ACCESS_FILE)
  59. if data then
  60. return textutils.unserialize(data) or {}
  61. end
  62. return {}
  63. end
  64.  
  65. local function saveAccessList(list)
  66. ndisk.write(ACCESS_FILE, textutils.serialize(list))
  67. end
  68.  
  69. local function hasAccess(studentId)
  70. if studentId == ADMIN_ACCOUNT then return true end
  71. local list = loadAccessList()
  72. for _, id in ipairs(list) do
  73. if id == studentId then return true end
  74. end
  75. return false
  76. end
  77.  
  78. local function grantAccess(studentId)
  79. if studentId == ADMIN_ACCOUNT then return true end
  80. local list = loadAccessList()
  81. for _, id in ipairs(list) do
  82. if id == studentId then return true end -- Already has access
  83. end
  84. table.insert(list, studentId)
  85. saveAccessList(list)
  86. return true
  87. end
  88.  
  89. local function revokeAccess(studentId)
  90. if studentId == ADMIN_ACCOUNT then return false end -- Can't revoke admin
  91. local list = loadAccessList()
  92. local newList = {}
  93. local found = false
  94. for _, id in ipairs(list) do
  95. if id ~= studentId then
  96. table.insert(newList, id)
  97. else
  98. found = true
  99. end
  100. end
  101. if found then
  102. saveAccessList(newList)
  103. end
  104. return found
  105. end
  106.  
  107. local function verifyLogin(studentId, password)
  108. -- Direct NDisk verification (we're on the server)
  109. local passPath = "students/" .. studentId .. "/password.txt"
  110. local storedPass = ndisk.read(passPath)
  111. if storedPass then
  112. -- Trim whitespace
  113. storedPass = storedPass:gsub("^%s*(.-)%s*$", "%1")
  114. return storedPass == password
  115. end
  116. return false
  117. end
  118.  
  119. -- ============================================
  120. -- UTILITY FUNCTIONS
  121. -- ============================================
  122.  
  123. local function clear()
  124. term.clear()
  125. term.setCursorPos(1, 1)
  126. end
  127.  
  128. local function centerText(text, y)
  129. local w, _ = term.getSize()
  130. term.setCursorPos(math.floor((w - #text) / 2) + 1, y)
  131. term.write(text)
  132. end
  133.  
  134. local function drawHeader(title)
  135. clear()
  136. local w, _ = term.getSize()
  137. term.setTextColor(colors.cyan)
  138. print(string.rep("=", w))
  139. centerText("P A L A N T I R", 2)
  140. term.setCursorPos(1, 3)
  141. print(string.rep("=", w))
  142. term.setTextColor(colors.yellow)
  143. centerText(title, 4)
  144. term.setTextColor(colors.white)
  145. print("")
  146. print("")
  147. end
  148.  
  149. local function prompt(msg)
  150. term.setTextColor(colors.gray)
  151. write(msg or "> ")
  152. term.setTextColor(colors.white)
  153. return read()
  154. end
  155.  
  156. local function pause()
  157. term.setTextColor(colors.gray)
  158. print("")
  159. print("Press any key to continue...")
  160. os.pullEvent("key")
  161. end
  162.  
  163. local function getStudentList()
  164. local students = ndisk.list("students")
  165. return students or {}
  166. end
  167.  
  168. -- Node health check helper (used by both space command and RAID status)
  169. local function checkNodeHealth(nodeId)
  170. local modemNdisk = peripheral.find("modem")
  171. if not modemNdisk then return false, 0 end
  172.  
  173. modemNdisk.open(5)
  174. local requestId = os.getComputerID() .. "_health_" .. os.epoch("utc") .. "_" .. math.random(10000)
  175. modemNdisk.transmit(5, 5, {
  176. targetNode = nodeId,
  177. id = requestId,
  178. action = "getSpace",
  179. secret = NDISK_SECRET
  180. })
  181.  
  182. local timer = os.startTimer(2)
  183. while true do
  184. local event, p1, p2, p3, p4 = os.pullEvent()
  185. if event == "modem_message" and type(p4) == "table" and p4.id == requestId then
  186. return p4.response and p4.response.success, p4.response and p4.response.space or 0
  187. elseif event == "timer" and p1 == timer then
  188. return false, 0
  189. end
  190. end
  191. end
  192.  
  193. -- ============================================
  194. -- APP 1: NDISK TERMINAL (inline version)
  195. -- ============================================
  196.  
  197. local function appNDiskTerminal()
  198. drawHeader("NDisk Terminal")
  199.  
  200. print("Commands: list, read, write, delete, space, help, exit")
  201. print("")
  202.  
  203. while true do
  204. local input = prompt("ndisk> ")
  205. local command = {}
  206. for word in input:gmatch("%S+") do
  207. table.insert(command, word)
  208. end
  209.  
  210. local cmd = command[1]
  211. local arg1 = command[2]
  212.  
  213. if cmd == "list" then
  214. local path = arg1 or ""
  215. local items = ndisk.list(path)
  216. print("Listing '" .. path .. "':")
  217. if #items == 0 then
  218. print(" (No files found)")
  219. else
  220. for _, item in ipairs(items) do
  221. print(" " .. item)
  222. end
  223. end
  224.  
  225. elseif cmd == "read" and arg1 then
  226. local content, err = ndisk.read(arg1)
  227. if content then
  228. print("--- BEGIN ---")
  229. print(content)
  230. print("--- END ---")
  231. else
  232. print("Error: " .. (err or "Unknown"))
  233. end
  234.  
  235. elseif cmd == "write" and arg1 then
  236. print("Enter content (type EOF to finish):")
  237. local content = ""
  238. while true do
  239. local line = read()
  240. if line == "EOF" then break end
  241. content = content .. line .. "\n"
  242. end
  243. local success, err = ndisk.write(arg1, content)
  244. print(success and "Written." or ("Error: " .. (err or "Unknown")))
  245.  
  246. elseif cmd == "delete" and arg1 then
  247. local success, err = ndisk.delete(arg1)
  248. print(success and "Deleted." or ("Error: " .. (err or "Unknown")))
  249.  
  250. elseif cmd == "space" then
  251. print("")
  252. term.setTextColor(colors.cyan)
  253. print("STORAGE DETAILS:")
  254. term.setTextColor(colors.white)
  255. print("")
  256.  
  257. -- Load node IDs
  258. local nodeIds = {}
  259. if fs.exists("ids.txt") then
  260. local file = fs.open("ids.txt", "r")
  261. for line in file.readLine do
  262. local id, num = line:match("(%d+)%s+(%d+)")
  263. if id and num then
  264. nodeIds[tonumber(num)] = tonumber(id)
  265. end
  266. end
  267. file.close()
  268. end
  269.  
  270. local sortedNums = {}
  271. for num, _ in pairs(nodeIds) do
  272. table.insert(sortedNums, num)
  273. end
  274. table.sort(sortedNums)
  275.  
  276. local totalRaw = 0
  277. local onlineNodes = 0
  278.  
  279. for _, num in ipairs(sortedNums) do
  280. local nodeId = nodeIds[num]
  281. local isHealthy, freeSpace = checkNodeHealth(nodeId)
  282.  
  283. if isHealthy then
  284. onlineNodes = onlineNodes + 1
  285. totalRaw = totalRaw + freeSpace
  286. -- Estimate used (disk is ~127KB total on floppy)
  287. local diskSize = 127 * 1024
  288. local used = diskSize - freeSpace
  289. local pctUsed = math.floor((used / diskSize) * 100)
  290.  
  291. term.setTextColor(colors.lime)
  292. print(" Node " .. num .. " (ID:" .. nodeId .. ")")
  293. term.setTextColor(colors.white)
  294. print(" Free: " .. math.floor(freeSpace/1024) .. " KB | Used: " .. math.floor(used/1024) .. " KB (" .. pctUsed .. "%)")
  295. else
  296. term.setTextColor(colors.red)
  297. print(" Node " .. num .. " (ID:" .. nodeId .. "): OFFLINE")
  298. term.setTextColor(colors.white)
  299. end
  300. end
  301.  
  302. print("")
  303. term.setTextColor(colors.cyan)
  304. print("RAID10 SUMMARY:")
  305. term.setTextColor(colors.white)
  306.  
  307. local raidStatus = ndisk.getRaidStatus()
  308. local usableSpace = math.floor(totalRaw / 2) -- RAID10 halves capacity
  309.  
  310. print(" Mode: " .. raidStatus.mode)
  311. print(" Online Nodes: " .. onlineNodes .. "/" .. raidStatus.nodeCount)
  312. print(" Raw Free: " .. math.floor(totalRaw/1024) .. " KB")
  313. print(" Usable Free (after mirroring): " .. math.floor(usableSpace/1024) .. " KB")
  314. print("")
  315.  
  316. elseif cmd == "help" then
  317. print("list [path], read <path>, write <path>,")
  318. print("delete <path>, space, exit")
  319.  
  320. elseif cmd == "exit" or cmd == "q" then
  321. break
  322.  
  323. elseif cmd and #cmd > 0 then
  324. print("Unknown command. Type 'help'.")
  325. end
  326. end
  327. end
  328.  
  329. -- ============================================
  330. -- APP 2: EMAIL SCRAPER
  331. -- ============================================
  332.  
  333. local function loadAllEmails()
  334. local allEmails = {}
  335. local students = getStudentList()
  336.  
  337. for _, studentId in ipairs(students) do
  338. local emailPath = "students/" .. studentId .. "/emails.txt"
  339. local data = ndisk.read(emailPath)
  340. if data then
  341. local emails = textutils.unserialize(data)
  342. if emails then
  343. for _, email in ipairs(emails) do
  344. email.recipient = studentId
  345. table.insert(allEmails, email)
  346. end
  347. end
  348. end
  349. end
  350.  
  351. -- Sort by timestamp (newest first)
  352. table.sort(allEmails, function(a, b)
  353. return (a.timestamp or 0) > (b.timestamp or 0)
  354. end)
  355.  
  356. return allEmails
  357. end
  358.  
  359. local function displayEmail(email, index)
  360. term.setTextColor(colors.yellow)
  361. print("[" .. (index or "?") .. "] " .. (email.sender or "?") .. " -> " .. (email.recipient or "?"))
  362. term.setTextColor(colors.white)
  363. print(" Subject: " .. (email.subject or "(no subject)"))
  364. term.setTextColor(colors.lightGray)
  365. local body = email.body or ""
  366. if #body > 60 then body = body:sub(1, 57) .. "..." end
  367. print(" " .. body)
  368. term.setTextColor(colors.white)
  369. end
  370.  
  371. local function appEmailScraper()
  372. drawHeader("Email Scraper")
  373.  
  374. print("Commands:")
  375. print(" latest [n] - Show latest n emails (default 5)")
  376. print(" student <id> - All emails to/from student")
  377. print(" search <word> - Search all emails for keyword")
  378. print(" read <n> - Read full email by index")
  379. print(" exit - Return to main menu")
  380. print("")
  381.  
  382. local cachedEmails = {}
  383.  
  384. while true do
  385. local input = prompt("email> ")
  386. local parts = {}
  387. for word in input:gmatch("%S+") do
  388. table.insert(parts, word)
  389. end
  390.  
  391. local cmd = parts[1]
  392. local arg = parts[2]
  393.  
  394. if cmd == "latest" then
  395. local count = tonumber(arg) or 5
  396. print("Loading emails...")
  397. cachedEmails = loadAllEmails()
  398. print("")
  399. term.setTextColor(colors.cyan)
  400. print("LATEST " .. math.min(count, #cachedEmails) .. " EMAILS:")
  401. term.setTextColor(colors.white)
  402. print("")
  403. for i = 1, math.min(count, #cachedEmails) do
  404. displayEmail(cachedEmails[i], i)
  405. print("")
  406. end
  407.  
  408. elseif cmd == "student" and arg then
  409. print("Loading emails for " .. arg .. "...")
  410. local allEmails = loadAllEmails()
  411. cachedEmails = {}
  412. for _, email in ipairs(allEmails) do
  413. if email.sender == arg or email.recipient == arg then
  414. table.insert(cachedEmails, email)
  415. end
  416. end
  417. print("")
  418. term.setTextColor(colors.cyan)
  419. print("EMAILS FOR: " .. arg .. " (" .. #cachedEmails .. " found)")
  420. term.setTextColor(colors.white)
  421. print("")
  422. for i, email in ipairs(cachedEmails) do
  423. displayEmail(email, i)
  424. print("")
  425. if i >= 10 then
  426. print("... and " .. (#cachedEmails - 10) .. " more")
  427. break
  428. end
  429. end
  430.  
  431. elseif cmd == "search" and arg then
  432. local keyword = table.concat(parts, " ", 2):lower()
  433. print("Searching for '" .. keyword .. "'...")
  434. local allEmails = loadAllEmails()
  435. cachedEmails = {}
  436. for _, email in ipairs(allEmails) do
  437. local haystack = ((email.subject or "") .. " " .. (email.body or "")):lower()
  438. if haystack:find(keyword, 1, true) then
  439. table.insert(cachedEmails, email)
  440. end
  441. end
  442. print("")
  443. term.setTextColor(colors.cyan)
  444. print("SEARCH: '" .. keyword .. "' (" .. #cachedEmails .. " matches)")
  445. term.setTextColor(colors.white)
  446. print("")
  447. for i, email in ipairs(cachedEmails) do
  448. displayEmail(email, i)
  449. print("")
  450. if i >= 10 then
  451. print("... and " .. (#cachedEmails - 10) .. " more")
  452. break
  453. end
  454. end
  455.  
  456. elseif cmd == "read" and arg then
  457. local idx = tonumber(arg)
  458. if idx and cachedEmails[idx] then
  459. local email = cachedEmails[idx]
  460. print("")
  461. term.setTextColor(colors.yellow)
  462. print("From: " .. (email.sender or "?"))
  463. print("To: " .. (email.recipient or "?"))
  464. print("Subject: " .. (email.subject or "(none)"))
  465. term.setTextColor(colors.white)
  466. print(string.rep("-", 40))
  467. print(email.body or "(empty)")
  468. print(string.rep("-", 40))
  469. else
  470. print("Invalid index. Run a search first.")
  471. end
  472.  
  473. elseif cmd == "exit" or cmd == "q" then
  474. break
  475.  
  476. elseif cmd and #cmd > 0 then
  477. print("Unknown. Try: latest, student, search, read, exit")
  478. end
  479. end
  480. end
  481.  
  482. -- ============================================
  483. -- APP 3: LOCATION INTEL
  484. -- ============================================
  485.  
  486. local function parseLocationHistory(studentId)
  487. local historyPath = "students/" .. studentId .. "/location_history.txt"
  488. local data = ndisk.read(historyPath)
  489. if not data then return {} end
  490.  
  491. local locations = {}
  492. for line in data:gmatch("[^\n]+") do
  493. local timestamp, coords = line:match("([^|]+)|(.+)")
  494. if coords then
  495. local x, y, z = coords:match("([%-?%d]+),([%-?%d]+),([%-?%d]+)")
  496. if x and y and z then
  497. table.insert(locations, {
  498. timestamp = tonumber(timestamp) or 0,
  499. x = tonumber(x),
  500. y = tonumber(y),
  501. z = tonumber(z)
  502. })
  503. end
  504. end
  505. end
  506. return locations
  507. end
  508.  
  509. local function clusterLocations(locations)
  510. local clusters = {} -- key = "gridX,gridZ" -> count
  511.  
  512. for _, loc in ipairs(locations) do
  513. local gridX = math.floor(loc.x / GRID_SIZE) * GRID_SIZE
  514. local gridZ = math.floor(loc.z / GRID_SIZE) * GRID_SIZE
  515. local key = gridX .. "," .. gridZ
  516. clusters[key] = (clusters[key] or 0) + 1
  517. end
  518.  
  519. -- Convert to sorted list
  520. local hotspots = {}
  521. for key, count in pairs(clusters) do
  522. local gx, gz = key:match("([%-?%d]+),([%-?%d]+)")
  523. table.insert(hotspots, {
  524. x = tonumber(gx) + math.floor(GRID_SIZE/2), -- Center of grid
  525. z = tonumber(gz) + math.floor(GRID_SIZE/2),
  526. visits = count
  527. })
  528. end
  529.  
  530. table.sort(hotspots, function(a, b) return a.visits > b.visits end)
  531. return hotspots
  532. end
  533.  
  534. local function appLocationIntel()
  535. drawHeader("Location Intel")
  536.  
  537. print("Commands:")
  538. print(" list - Show all tracked students")
  539. print(" analyze <id> - Show hotspots for student")
  540. print(" track <id> - Show last known location")
  541. print(" region <x> <z> - Who visits this area?")
  542. print(" exit - Return to main menu")
  543. print("")
  544.  
  545. while true do
  546. local input = prompt("loc> ")
  547. local parts = {}
  548. for word in input:gmatch("%S+") do
  549. table.insert(parts, word)
  550. end
  551.  
  552. local cmd = parts[1]
  553. local arg1 = parts[2]
  554. local arg2 = parts[3]
  555.  
  556. if cmd == "list" then
  557. local students = getStudentList()
  558. print("")
  559. term.setTextColor(colors.cyan)
  560. print("TRACKED STUDENTS (" .. #students .. "):")
  561. term.setTextColor(colors.white)
  562. for i, s in ipairs(students) do
  563. write(s .. " ")
  564. if i % 5 == 0 then print("") end
  565. end
  566. print("")
  567.  
  568. elseif cmd == "analyze" and arg1 then
  569. print("Analyzing " .. arg1 .. "...")
  570. local locations = parseLocationHistory(arg1)
  571. if #locations == 0 then
  572. print("No location data for " .. arg1)
  573. else
  574. local hotspots = clusterLocations(locations)
  575. print("")
  576. term.setTextColor(colors.cyan)
  577. print(arg1:upper() .. "'s HOTSPOTS (" .. #locations .. " total pings):")
  578. term.setTextColor(colors.white)
  579. print("")
  580. for i = 1, math.min(10, #hotspots) do
  581. local h = hotspots[i]
  582. local label = ""
  583. if i == 1 then label = " <- likely base" end
  584. if h.visits > 20 then label = label .. " [FREQUENT]" end
  585. print(string.format(" #%d: %d, *, %d (%d visits)%s",
  586. i, h.x, h.z, h.visits, label))
  587. end
  588. end
  589. print("")
  590.  
  591. elseif cmd == "track" and arg1 then
  592. local locations = parseLocationHistory(arg1)
  593. if #locations == 0 then
  594. print("No location data for " .. arg1)
  595. else
  596. -- Most recent is last in list (chronological) or first if sorted
  597. local latest = locations[#locations]
  598. local age = math.floor((os.epoch("local") - latest.timestamp) / 60000)
  599. print("")
  600. term.setTextColor(colors.yellow)
  601. print(arg1:upper() .. " LAST SEEN:")
  602. term.setTextColor(colors.white)
  603. print(string.format(" Location: %d, %d, %d", latest.x, latest.y, latest.z))
  604. print(string.format(" Time: %d minutes ago", age))
  605. end
  606. print("")
  607.  
  608. elseif cmd == "region" and arg1 and arg2 then
  609. local targetX = tonumber(arg1)
  610. local targetZ = tonumber(arg2)
  611. if not targetX or not targetZ then
  612. print("Usage: region <x> <z>")
  613. else
  614. local gridX = math.floor(targetX / GRID_SIZE) * GRID_SIZE
  615. local gridZ = math.floor(targetZ / GRID_SIZE) * GRID_SIZE
  616. print("Scanning region around " .. gridX .. ", " .. gridZ .. "...")
  617.  
  618. local visitors = {}
  619. local students = getStudentList()
  620. for _, studentId in ipairs(students) do
  621. local locations = parseLocationHistory(studentId)
  622. local count = 0
  623. for _, loc in ipairs(locations) do
  624. local lx = math.floor(loc.x / GRID_SIZE) * GRID_SIZE
  625. local lz = math.floor(loc.z / GRID_SIZE) * GRID_SIZE
  626. if lx == gridX and lz == gridZ then
  627. count = count + 1
  628. end
  629. end
  630. if count > 0 then
  631. table.insert(visitors, {id = studentId, visits = count})
  632. end
  633. end
  634.  
  635. table.sort(visitors, function(a, b) return a.visits > b.visits end)
  636.  
  637. print("")
  638. term.setTextColor(colors.cyan)
  639. print("VISITORS TO REGION " .. gridX .. ", " .. gridZ .. ":")
  640. term.setTextColor(colors.white)
  641. if #visitors == 0 then
  642. print(" No one has been here")
  643. else
  644. for _, v in ipairs(visitors) do
  645. print(" " .. v.id .. ": " .. v.visits .. " visits")
  646. end
  647. end
  648. end
  649. print("")
  650.  
  651. elseif cmd == "exit" or cmd == "q" then
  652. break
  653.  
  654. elseif cmd and #cmd > 0 then
  655. print("Unknown. Try: list, analyze, track, region, exit")
  656. end
  657. end
  658. end
  659.  
  660. -- ============================================
  661. -- APP 4: TRANSACTION MONITOR
  662. -- ============================================
  663.  
  664. local function loadTransactions()
  665. local txPath = "transactions/log.txt"
  666. local data = ndisk.read(txPath)
  667. if not data then return {} end
  668.  
  669. local transactions = {}
  670. for line in data:gmatch("[^\n]+") do
  671. local timestamp, studentId, action, amount, balance =
  672. line:match("([^|]+)|([^|]+)|([^|]+)|([^|]+)|([^|]*)")
  673. if timestamp and studentId then
  674. table.insert(transactions, {
  675. timestamp = tonumber(timestamp) or 0,
  676. student = studentId,
  677. action = action,
  678. amount = tonumber(amount) or 0,
  679. balance = tonumber(balance) or 0
  680. })
  681. end
  682. end
  683.  
  684. return transactions
  685. end
  686.  
  687. local function appTransactionMonitor()
  688. drawHeader("Transaction Monitor")
  689.  
  690. print("Commands:")
  691. print(" recent [n] - Show last n transactions")
  692. print(" stats - Show transaction statistics")
  693. print(" suspicious - Show flagged transactions")
  694. print(" student <id> - Transactions for student")
  695. print(" exit - Return to main menu")
  696. print("")
  697.  
  698. while true do
  699. local input = prompt("tx> ")
  700. local parts = {}
  701. for word in input:gmatch("%S+") do
  702. table.insert(parts, word)
  703. end
  704.  
  705. local cmd = parts[1]
  706. local arg = parts[2]
  707.  
  708. if cmd == "recent" then
  709. local count = tonumber(arg) or 10
  710. local txs = loadTransactions()
  711.  
  712. -- Filter out admin
  713. local filtered = {}
  714. for i = #txs, 1, -1 do -- Reverse for newest first
  715. if txs[i].student ~= ADMIN_ACCOUNT then
  716. table.insert(filtered, txs[i])
  717. if #filtered >= count then break end
  718. end
  719. end
  720.  
  721. print("")
  722. term.setTextColor(colors.cyan)
  723. print("RECENT TRANSACTIONS:")
  724. term.setTextColor(colors.white)
  725. for _, tx in ipairs(filtered) do
  726. local age = math.floor((os.epoch("local") - tx.timestamp) / 60000)
  727. local sign = tx.action == "deposit" and "+" or "-"
  728. print(string.format(" %s %s%d E (%dm ago) bal:%d",
  729. tx.student, sign, tx.amount, age, tx.balance))
  730. end
  731. print("")
  732.  
  733. elseif cmd == "stats" then
  734. local txs = loadTransactions()
  735. local total = 0
  736. local count = 0
  737. local deposits = 0
  738. local withdrawals = 0
  739.  
  740. for _, tx in ipairs(txs) do
  741. if tx.student ~= ADMIN_ACCOUNT then
  742. total = total + tx.amount
  743. count = count + 1
  744. if tx.action == "deposit" then
  745. deposits = deposits + tx.amount
  746. else
  747. withdrawals = withdrawals + tx.amount
  748. end
  749. end
  750. end
  751.  
  752. local avg = count > 0 and (total / count) or 0
  753.  
  754. print("")
  755. term.setTextColor(colors.cyan)
  756. print("TRANSACTION STATISTICS:")
  757. term.setTextColor(colors.white)
  758. print(" Total transactions: " .. count)
  759. print(" Total volume: " .. total .. " E")
  760. print(" Average amount: " .. string.format("%.1f", avg) .. " E")
  761. print(" Total deposits: " .. deposits .. " E")
  762. print(" Total withdrawals: " .. withdrawals .. " E")
  763. print(" Anomaly threshold: >" .. string.format("%.1f", avg * ANOMALY_MULTIPLIER) .. " E")
  764. print("")
  765.  
  766. elseif cmd == "suspicious" then
  767. local txs = loadTransactions()
  768. local total = 0
  769. local count = 0
  770.  
  771. -- Calculate average first
  772. for _, tx in ipairs(txs) do
  773. if tx.student ~= ADMIN_ACCOUNT then
  774. total = total + tx.amount
  775. count = count + 1
  776. end
  777. end
  778. local avg = count > 0 and (total / count) or 0
  779. local threshold = avg * ANOMALY_MULTIPLIER
  780. if threshold < 20 then threshold = 20 end -- Minimum threshold
  781.  
  782. -- Find suspicious
  783. local suspicious = {}
  784. for i = #txs, 1, -1 do
  785. local tx = txs[i]
  786. if tx.student ~= ADMIN_ACCOUNT and tx.amount > threshold then
  787. table.insert(suspicious, tx)
  788. if #suspicious >= 20 then break end
  789. end
  790. end
  791.  
  792. print("")
  793. term.setTextColor(colors.red)
  794. print("SUSPICIOUS TRANSACTIONS (>" .. string.format("%.0f", threshold) .. " E):")
  795. term.setTextColor(colors.white)
  796. if #suspicious == 0 then
  797. print(" None found")
  798. else
  799. for _, tx in ipairs(suspicious) do
  800. local age = math.floor((os.epoch("local") - tx.timestamp) / 60000)
  801. local sign = tx.action == "deposit" and "+" or "-"
  802. term.setTextColor(colors.orange)
  803. print(string.format(" [!] %s %s%d E (%dm ago)",
  804. tx.student, sign, tx.amount, age))
  805. end
  806. end
  807. term.setTextColor(colors.white)
  808. print("")
  809.  
  810. elseif cmd == "student" and arg then
  811. local txs = loadTransactions()
  812. local filtered = {}
  813. for i = #txs, 1, -1 do
  814. if txs[i].student == arg then
  815. table.insert(filtered, txs[i])
  816. if #filtered >= 20 then break end
  817. end
  818. end
  819.  
  820. print("")
  821. term.setTextColor(colors.cyan)
  822. print("TRANSACTIONS FOR: " .. arg)
  823. term.setTextColor(colors.white)
  824. if #filtered == 0 then
  825. print(" No transactions found")
  826. else
  827. for _, tx in ipairs(filtered) do
  828. local age = math.floor((os.epoch("local") - tx.timestamp) / 60000)
  829. local sign = tx.action == "deposit" and "+" or "-"
  830. print(string.format(" %s%d E (%dm ago) bal:%d",
  831. sign, tx.amount, age, tx.balance))
  832. end
  833. end
  834. print("")
  835.  
  836. elseif cmd == "exit" or cmd == "q" then
  837. break
  838.  
  839. elseif cmd and #cmd > 0 then
  840. print("Unknown. Try: recent, stats, suspicious, student, exit")
  841. end
  842. end
  843. end
  844.  
  845. -- ============================================
  846. -- APP 5: RAID STATUS DASHBOARD
  847. -- ============================================
  848.  
  849. local function appRaidStatus()
  850. drawHeader("RAID Status Dashboard")
  851.  
  852. print("Commands:")
  853. print(" status - Show overall RAID health")
  854. print(" nodes - Check all node health")
  855. print(" files - Show file redundancy status")
  856. print(" refresh - Refresh node discovery")
  857. print(" exit - Return to main menu")
  858. print("")
  859.  
  860. while true do
  861. local input = prompt("raid> ")
  862. if not input then input = "" end
  863. local parts = {}
  864. for word in input:gmatch("%S+") do
  865. table.insert(parts, word)
  866. end
  867.  
  868. local cmd = parts[1]
  869.  
  870. if cmd == "status" then
  871. print("")
  872. local raidStatus = ndisk.getRaidStatus()
  873.  
  874. term.setTextColor(colors.cyan)
  875. print("RAID CLUSTER STATUS:")
  876. term.setTextColor(colors.white)
  877. print("")
  878.  
  879. -- Mode
  880. if raidStatus.enabled then
  881. term.setTextColor(colors.lime)
  882. print(" Mode: RAID10 (Mirrored)")
  883. else
  884. term.setTextColor(colors.red)
  885. print(" Mode: DEGRADED (Single-copy)")
  886. end
  887. term.setTextColor(colors.white)
  888.  
  889. print(" Total Nodes: " .. raidStatus.nodeCount)
  890. print(" Max Failures: " .. raidStatus.maxFailures .. " nodes can go down")
  891.  
  892. -- Free space
  893. local freeSpace = ndisk.getFreeSpace()
  894. print(" Usable Space: " .. math.floor(freeSpace / 1024) .. " KB")
  895.  
  896. print("")
  897.  
  898. elseif cmd == "nodes" then
  899. print("")
  900. print("Checking node health...")
  901. print("")
  902.  
  903. -- Load node IDs from ids.txt
  904. local nodeIds = {}
  905. if fs.exists("ids.txt") then
  906. local file = fs.open("ids.txt", "r")
  907. for line in file.readLine do
  908. local id, num = line:match("(%d+)%s+(%d+)")
  909. if id and num then
  910. nodeIds[tonumber(num)] = tonumber(id)
  911. end
  912. end
  913. file.close()
  914. end
  915.  
  916. local healthy = 0
  917. local unhealthy = 0
  918. local sortedNums = {}
  919. for num, _ in pairs(nodeIds) do
  920. table.insert(sortedNums, num)
  921. end
  922. table.sort(sortedNums)
  923.  
  924. term.setTextColor(colors.cyan)
  925. print("NODE HEALTH:")
  926. term.setTextColor(colors.white)
  927.  
  928. for _, num in ipairs(sortedNums) do
  929. local nodeId = nodeIds[num]
  930. local isHealthy, space = checkNodeHealth(nodeId)
  931.  
  932. if isHealthy then
  933. healthy = healthy + 1
  934. term.setTextColor(colors.lime)
  935. print(" [" .. num .. "] Node " .. nodeId .. ": OK (" .. math.floor(space/1024) .. " KB free)")
  936. else
  937. unhealthy = unhealthy + 1
  938. term.setTextColor(colors.red)
  939. print(" [" .. num .. "] Node " .. nodeId .. ": OFFLINE")
  940. end
  941. end
  942.  
  943. term.setTextColor(colors.white)
  944. print("")
  945. print("Summary: " .. healthy .. " healthy, " .. unhealthy .. " offline")
  946.  
  947. local raidStatus = ndisk.getRaidStatus()
  948. if unhealthy > 0 and unhealthy <= raidStatus.maxFailures then
  949. term.setTextColor(colors.yellow)
  950. print("WARNING: Running degraded but still functional")
  951. elseif unhealthy > raidStatus.maxFailures then
  952. term.setTextColor(colors.red)
  953. print("CRITICAL: Data at risk! Too many nodes offline!")
  954. else
  955. term.setTextColor(colors.lime)
  956. print("All nodes healthy - full redundancy active")
  957. end
  958. term.setTextColor(colors.white)
  959. print("")
  960.  
  961. elseif cmd == "files" then
  962. print("")
  963. print("Analyzing file redundancy...")
  964.  
  965. -- Load chunk map and check RAID status per file
  966. local fileStats = {raid10 = 0, single = 0, total = 0}
  967.  
  968. if fs.exists("chunklocation.txt") then
  969. local file = fs.open("chunklocation.txt", "r")
  970. local files = {}
  971. for line in file.readLine do
  972. local parts = {}
  973. for part in line:gmatch("%S+") do
  974. table.insert(parts, part)
  975. end
  976. if #parts >= 3 then
  977. local filename = parts[1]
  978. local hasRaid = #parts >= 4 and parts[3] ~= parts[4]
  979. if not files[filename] then
  980. files[filename] = {hasRaid = true}
  981. fileStats.total = fileStats.total + 1
  982. end
  983. if not hasRaid then
  984. files[filename].hasRaid = false
  985. end
  986. end
  987. end
  988. file.close()
  989.  
  990. for _, info in pairs(files) do
  991. if info.hasRaid then
  992. fileStats.raid10 = fileStats.raid10 + 1
  993. else
  994. fileStats.single = fileStats.single + 1
  995. end
  996. end
  997. end
  998.  
  999. print("")
  1000. term.setTextColor(colors.cyan)
  1001. print("FILE REDUNDANCY STATUS:")
  1002. term.setTextColor(colors.white)
  1003. print(" Total files: " .. fileStats.total)
  1004.  
  1005. term.setTextColor(colors.lime)
  1006. print(" RAID10 protected: " .. fileStats.raid10)
  1007.  
  1008. if fileStats.single > 0 then
  1009. term.setTextColor(colors.yellow)
  1010. print(" Single-copy (at risk): " .. fileStats.single)
  1011. print("")
  1012. print(" Run ndisk_migrate_raid10 to protect these files")
  1013. else
  1014. print("")
  1015. term.setTextColor(colors.lime)
  1016. print(" All files have RAID10 protection!")
  1017. end
  1018. term.setTextColor(colors.white)
  1019. print("")
  1020.  
  1021. elseif cmd == "refresh" then
  1022. print("")
  1023. print("Refreshing node discovery...")
  1024. ndisk.refreshNodes()
  1025. local raidStatus = ndisk.getRaidStatus()
  1026. term.setTextColor(colors.lime)
  1027. print("Found " .. raidStatus.nodeCount .. " nodes")
  1028. term.setTextColor(colors.white)
  1029. print("")
  1030.  
  1031. elseif cmd == "exit" or cmd == "q" then
  1032. break
  1033.  
  1034. elseif cmd and #cmd > 0 then
  1035. print("Unknown. Try: status, nodes, files, refresh, exit")
  1036. end
  1037. end
  1038. end
  1039.  
  1040. -- ============================================
  1041. -- MAIN MENU
  1042. -- ============================================
  1043.  
  1044. local function appAccessManagement()
  1045. if currentUser ~= ADMIN_ACCOUNT then
  1046. print("Access denied. Admin only.")
  1047. pause()
  1048. return
  1049. end
  1050.  
  1051. drawHeader("Access Management")
  1052.  
  1053. print("Commands:")
  1054. print(" list - Show authorized users")
  1055. print(" grant <id> - Grant access to user")
  1056. print(" revoke <id> - Revoke access from user")
  1057. print(" exit - Return to main menu")
  1058. print("")
  1059.  
  1060. while true do
  1061. local input = prompt("access> ")
  1062. local parts = {}
  1063. for word in input:gmatch("%S+") do
  1064. table.insert(parts, word)
  1065. end
  1066.  
  1067. local cmd = parts[1]
  1068. local arg = parts[2]
  1069.  
  1070. if cmd == "list" then
  1071. local list = loadAccessList()
  1072. print("")
  1073. term.setTextColor(colors.cyan)
  1074. print("AUTHORIZED USERS:")
  1075. term.setTextColor(colors.yellow)
  1076. print(" " .. ADMIN_ACCOUNT .. " (master)")
  1077. term.setTextColor(colors.white)
  1078. for _, id in ipairs(list) do
  1079. print(" " .. id)
  1080. end
  1081. if #list == 0 then
  1082. print(" (no additional users)")
  1083. end
  1084. print("")
  1085.  
  1086. elseif cmd == "grant" and arg then
  1087. if grantAccess(arg) then
  1088. term.setTextColor(colors.lime)
  1089. print("Access granted to: " .. arg)
  1090. term.setTextColor(colors.white)
  1091. end
  1092.  
  1093. elseif cmd == "revoke" and arg then
  1094. if revokeAccess(arg) then
  1095. term.setTextColor(colors.orange)
  1096. print("Access revoked from: " .. arg)
  1097. term.setTextColor(colors.white)
  1098. else
  1099. print("User not found or cannot be revoked.")
  1100. end
  1101.  
  1102. elseif cmd == "exit" or cmd == "q" then
  1103. break
  1104.  
  1105. elseif cmd and #cmd > 0 then
  1106. print("Unknown. Try: list, grant, revoke, exit")
  1107. end
  1108. end
  1109. end
  1110.  
  1111.  
  1112. local function drawMainMenu()
  1113. clear()
  1114. local w, _ = term.getSize()
  1115.  
  1116. term.setTextColor(colors.cyan)
  1117. print(string.rep("=", w))
  1118. print("")
  1119. centerText("P A L A N T I R", 3)
  1120. term.setTextColor(colors.lightGray)
  1121. centerText("Intelligence Dashboard", 4)
  1122. term.setTextColor(colors.cyan)
  1123. print("")
  1124. term.setCursorPos(1, 6)
  1125. print(string.rep("=", w))
  1126.  
  1127. term.setTextColor(colors.white)
  1128. print("")
  1129.  
  1130. -- Show logged in user
  1131. term.setTextColor(colors.gray)
  1132. print(" Logged in as: " .. (currentUser or "unknown"))
  1133.  
  1134. -- Count some stats
  1135. local students = getStudentList()
  1136. print(" Students tracked: " .. #students)
  1137. print("")
  1138.  
  1139. term.setTextColor(colors.white)
  1140. print(" [1] NDisk Terminal")
  1141. print(" [2] Email Scraper")
  1142. print(" [3] Location Intel")
  1143. print(" [4] Transaction Monitor")
  1144. print(" [5] RAID Status")
  1145.  
  1146. -- Admin-only option
  1147. if currentUser == ADMIN_ACCOUNT then
  1148. term.setTextColor(colors.yellow)
  1149. print(" [6] Access Management")
  1150. end
  1151.  
  1152. print("")
  1153. term.setTextColor(colors.red)
  1154. print(" [Q] Logout")
  1155. term.setTextColor(colors.white)
  1156. print("")
  1157. end
  1158.  
  1159. local function loginScreen()
  1160. clear()
  1161. local w, _ = term.getSize()
  1162.  
  1163. term.setTextColor(colors.cyan)
  1164. print(string.rep("=", w))
  1165. centerText("P A L A N T I R", 2)
  1166. term.setCursorPos(1, 3)
  1167. print(string.rep("=", w))
  1168.  
  1169. term.setTextColor(colors.red)
  1170. print("")
  1171. centerText("AUTHORIZED ACCESS ONLY", 5)
  1172. term.setTextColor(colors.white)
  1173. print("")
  1174. print("")
  1175.  
  1176. write(" Username: ")
  1177. local username = read()
  1178. write(" Password: ")
  1179. local password = read("*")
  1180.  
  1181. print("")
  1182. print(" Verifying with server...")
  1183.  
  1184. -- Verify with server
  1185. if not verifyLogin(username, password) then
  1186. term.setTextColor(colors.red)
  1187. print(" Login failed!")
  1188. term.setTextColor(colors.white)
  1189. sleep(2)
  1190. return nil
  1191. end
  1192.  
  1193. -- Check access list
  1194. if not hasAccess(username) then
  1195. term.setTextColor(colors.red)
  1196. print(" Access denied. You are not authorized.")
  1197. term.setTextColor(colors.white)
  1198. sleep(2)
  1199. return nil
  1200. end
  1201.  
  1202. term.setTextColor(colors.lime)
  1203. print(" Access granted.")
  1204. term.setTextColor(colors.white)
  1205. sleep(1)
  1206.  
  1207. return username
  1208. end
  1209.  
  1210. local function mainLoop()
  1211. while true do
  1212. drawMainMenu()
  1213. local input = prompt("Select: ")
  1214.  
  1215. if input == "1" then
  1216. appNDiskTerminal()
  1217. elseif input == "2" then
  1218. appEmailScraper()
  1219. elseif input == "3" then
  1220. appLocationIntel()
  1221. elseif input == "4" then
  1222. appTransactionMonitor()
  1223. elseif input == "5" then
  1224. appRaidStatus()
  1225. elseif input == "6" and currentUser == ADMIN_ACCOUNT then
  1226. appAccessManagement()
  1227. elseif input:lower() == "q" or input:lower() == "logout" then
  1228. clear()
  1229. print("Logged out.")
  1230. currentUser = nil
  1231. break
  1232. end
  1233. end
  1234. end
  1235.  
  1236. local function main()
  1237. -- Keep running forever, return to login on logout
  1238. while true do
  1239. currentUser = loginScreen()
  1240. if currentUser then
  1241. mainLoop()
  1242. end
  1243. end
  1244. end
  1245.  
  1246. -- Run
  1247. main()
  1248.  
  1249.  
Advertisement
Comments
  • User was banned
Add Comment
Please, Sign In to add comment