MtnMCG

ndisk_repair

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