update and add tui for accesscontrol

This commit is contained in:
2025-10-12 16:50:48 +08:00
parent 069196dfbb
commit 1f85ef6aa2
6 changed files with 768 additions and 11 deletions

View File

@@ -3,10 +3,10 @@
"devenv": { "devenv": {
"locked": { "locked": {
"dir": "src/modules", "dir": "src/modules",
"lastModified": 1759939975, "lastModified": 1760162706,
"owner": "cachix", "owner": "cachix",
"repo": "devenv", "repo": "devenv",
"rev": "6eda3b7af3010d289e6e8e047435956fc80c1395", "rev": "0d5ad578728fe4bce66eb4398b8b1e66deceb4e4",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -1,11 +1,13 @@
{ pkgs, lib, config, inputs, ... }:
{ {
pkgs,
lib,
config,
inputs,
...
}: {
packages = with pkgs; [ packages = with pkgs; [
pnpm pnpm
craftos-pc craftos-pc
qwen-code
gemini-cli
]; ];
# https://devenv.sh/languages/ # https://devenv.sh/languages/

View File

@@ -2,10 +2,8 @@
inputs: inputs:
nixpkgs: nixpkgs:
url: github:cachix/devenv-nixpkgs/rolling url: github:cachix/devenv-nixpkgs/rolling
# If you're using non-OSS software, you can set allowUnfree to true. # If you're using non-OSS software, you can set allowUnfree to true.
# allowUnfree: true allowUnfree: true
# If you're willing to use a package that's vulnerable # If you're willing to use a package that's vulnerable
# permittedInsecurePackages: # permittedInsecurePackages:
# - "openssl-1.1.1w" # - "openssl-1.1.1w"

View File

@@ -0,0 +1,110 @@
/**
* Access Control Log Viewer
* Simple log viewer that allows launching the TUI with 'c' key
*/
import { launchAccessControlTUI } from "./tui";
const args = [...$vararg];
function displayLog(filepath: string, lines = 20) {
const [file] = io.open(filepath, "r");
if (!file) {
print(`Failed to open log file: ${filepath}`);
return;
}
const content = file.read("*a");
file.close();
if (content === null || content === undefined || content === "") {
print("Log file is empty");
return;
}
const logLines = content.split("\n");
const startIndex = Math.max(0, logLines.length - lines);
const displayLines = logLines.slice(startIndex);
term.clear();
term.setCursorPos(1, 1);
print("=== Access Control Log Viewer ===");
print("Press 'c' to open configuration TUI, 'q' to quit, 'r' to refresh");
print("==========================================");
print("");
for (const line of displayLines) {
if (line.trim() !== "") {
print(line);
}
}
print("");
print("==========================================");
print(`Showing last ${displayLines.length} lines of ${filepath}`);
}
function main(args: string[]) {
const logFilepath = args[0] || `${shell.dir()}/accesscontrol.log`;
const lines = args[1] ? parseInt(args[1]) : 20;
if (isNaN(lines) || lines <= 0) {
print("Usage: logviewer [logfile] [lines]");
print(" logfile - Path to log file (default: accesscontrol.log)");
print(" lines - Number of lines to display (default: 20)");
return;
}
let running = true;
// Initial display
displayLog(logFilepath, lines);
while (running) {
const [eventType, key] = os.pullEvent();
if (eventType === "key") {
if (key === keys.c) {
// Launch TUI
print("Launching Access Control TUI...");
try {
launchAccessControlTUI();
// Refresh display after TUI closes
displayLog(logFilepath, lines);
} catch (error) {
if (error === "TUI_CLOSE" || error === "Terminated") {
displayLog(logFilepath, lines);
} else {
print(`TUI error: ${String(error)}`);
os.sleep(2);
displayLog(logFilepath, lines);
}
}
} else if (key === keys.q) {
// Quit
running = false;
} else if (key === keys.r) {
// Refresh
displayLog(logFilepath, lines);
}
} else if (eventType === "terminate") {
running = false;
}
}
term.clear();
term.setCursorPos(1, 1);
print("Log viewer closed.");
}
try {
main(args);
} catch (error) {
if (error === "Terminated") {
print("Log viewer terminated by user.");
} else {
print("Error in log viewer:");
printError(error);
}
}

View File

@@ -1,13 +1,14 @@
import { CCLog, DAY } from "@/lib/ccLog"; import { CCLog, DAY } from "@/lib/ccLog";
import { ToastConfig, UserGroupConfig, loadConfig, setLog } from "./config"; import { ToastConfig, UserGroupConfig, loadConfig, setLog } from "./config";
import { createAccessControlCLI } from "./cli"; import { createAccessControlCLI } from "./cli";
import { launchAccessControlTUI } from "./tui";
import * as peripheralManager from "../lib/PeripheralManager"; import * as peripheralManager from "../lib/PeripheralManager";
const DEBUG = false; const DEBUG = false;
const args = [...$vararg]; const args = [...$vararg];
// Init Log // Init Log
const log = new CCLog("accesscontrol.log", DAY); const log = new CCLog("accesscontrol.log", true, DAY);
setLog(log); setLog(log);
// Load Config // Load Config
@@ -174,6 +175,21 @@ function mainLoop() {
} }
} }
function keyboardLoop() {
while (true) {
const [eventType, key] = os.pullEvent("key");
if (eventType === "key" && key === keys.c) {
log.info("Launching Access Control TUI...");
try {
launchAccessControlTUI();
log.info("TUI closed, resuming normal operation");
} catch (error) {
log.error(`TUI error: ${textutils.serialise(error as object)}`);
}
}
}
}
function main(args: string[]) { function main(args: string[]) {
log.info("Starting access control system, get args: " + args.join(", ")); log.info("Starting access control system, get args: " + args.join(", "));
if (args.length == 1) { if (args.length == 1) {
@@ -187,6 +203,9 @@ function main(args: string[]) {
groupNames, groupNames,
); );
print(
"Access Control System started. Press 'c' to open configuration TUI.",
);
parallel.waitForAll( parallel.waitForAll(
() => { () => {
mainLoop(); mainLoop();
@@ -197,12 +216,25 @@ function main(args: string[]) {
() => { () => {
watchLoop(); watchLoop();
}, },
() => {
keyboardLoop();
},
); );
return; return;
} else if (args[0] == "config") {
log.info("Launching Access Control TUI...");
try {
launchAccessControlTUI();
} catch (error) {
log.error(`TUI error: ${textutils.serialise(error as object)}`);
}
return;
} }
} }
print(`Usage: accesscontrol start`); print(`Usage: accesscontrol start | config`);
print(" start - Start the access control system with monitoring");
print(" config - Open configuration TUI");
} }
try { try {

View File

@@ -0,0 +1,615 @@
/**
* Access Control TUI Implementation
* A text-based user interface for configuring access control settings
*/
import {
createSignal,
createStore,
div,
label,
button,
input,
h1,
render,
Show,
For,
} from "../lib/ccTUI";
import {
AccessConfig,
UserGroupConfig,
loadConfig,
saveConfig,
} from "./config";
// Tab indices
const TABS = {
BASIC: 0,
GROUPS: 1,
WELCOME_TOAST: 2,
WARN_TOAST: 3,
NOTICE_TOAST: 4,
} as const;
type TabIndex = (typeof TABS)[keyof typeof TABS];
// Error dialog state
interface ErrorState {
show: boolean;
message: string;
}
/**
* Main TUI Application Component
*/
const AccessControlTUI = () => {
// Configuration state
const [config, setConfig] = createStore<AccessConfig>({} as AccessConfig);
// UI state
const [currentTab, setCurrentTab] = createSignal<TabIndex>(TABS.BASIC);
const [selectedGroupIndex, setSelectedGroupIndex] = createSignal(0);
const [errorState, setErrorState] = createStore<ErrorState>({
show: false,
message: "",
});
// New user input for group management
const [newUserName, setNewUserName] = createSignal("");
// Load configuration on initialization
const configFilepath = `${shell.dir()}/access.config.json`;
const loadedConfig = loadConfig(configFilepath);
setConfig(() => loadedConfig);
// Tab navigation functions
const tabNames = [
"Basic",
"Groups",
"Welcome Toast",
"Warn Toast",
"Notice Toast",
];
const showError = (message: string) => {
setErrorState("show", true);
setErrorState("message", message);
};
const hideError = () => {
setErrorState("show", false);
setErrorState("message", "");
};
// Validation functions
const validateNumber = (value: string): number | null => {
const num = parseInt(value);
return isNaN(num) ? null : num;
};
const validateTextComponent = (value: string): boolean => {
try {
const parsed = textutils.unserialiseJSON(value);
return parsed !== undefined && typeof parsed === "object";
} catch {
return false;
}
};
// Save configuration with validation
const handleSave = () => {
try {
const currentConfig = config();
// Validate numbers
if (
validateNumber(currentConfig.detectInterval?.toString() ?? "") === null
) {
showError("Invalid Detect Interval: must be a number");
return;
}
if (
validateNumber(currentConfig.watchInterval?.toString() ?? "") === null
) {
showError("Invalid Watch Interval: must be a number");
return;
}
if (
validateNumber(currentConfig.noticeTimes?.toString() ?? "") === null
) {
showError("Invalid Notice Times: must be a number");
return;
}
if (
validateNumber(currentConfig.detectRange?.toString() ?? "") === null
) {
showError("Invalid Detect Range: must be a number");
return;
}
// Validate text components for toast configs
const toastConfigs = [
{
name: "Welcome Toast Title",
value: currentConfig.welcomeToastConfig?.title,
},
{
name: "Welcome Toast Message",
value: currentConfig.welcomeToastConfig?.msg,
},
{
name: "Warn Toast Title",
value: currentConfig.warnToastConfig?.title,
},
{
name: "Warn Toast Message",
value: currentConfig.warnToastConfig?.msg,
},
{
name: "Notice Toast Title",
value: currentConfig.noticeToastConfig?.title,
},
{
name: "Notice Toast Message",
value: currentConfig.noticeToastConfig?.msg,
},
];
for (const toastConfig of toastConfigs) {
if (toastConfig.value != undefined) {
const serialized = textutils.serialiseJSON(toastConfig.value);
if (!validateTextComponent(serialized)) {
showError(
`Invalid ${toastConfig.name}: must be valid MinecraftTextComponent JSON`,
);
return;
}
}
}
// Save configuration
saveConfig(currentConfig, configFilepath);
showError("Configuration saved successfully!");
} catch (error) {
showError(`Failed to save configuration: ${String(error)}`);
}
};
// Add user to selected group
const addUser = () => {
const userName = newUserName().trim();
if (userName === "") return;
const groupIndex = selectedGroupIndex();
if (groupIndex === 0) {
// Admin group
const currentAdmin = config().adminGroupConfig;
setConfig("adminGroupConfig", {
...currentAdmin,
groupUsers: [...currentAdmin.groupUsers, userName],
});
} else {
// Regular group
const actualIndex = groupIndex - 1;
const currentGroups = config().usersGroups;
const currentGroup = currentGroups[actualIndex];
const newGroups = [...currentGroups];
newGroups[actualIndex] = {
...currentGroup,
groupUsers: [...(currentGroup?.groupUsers ?? []), userName],
};
setConfig("usersGroups", newGroups);
}
setNewUserName("");
};
// Remove user from selected group
const removeUser = (userName: string) => {
const groupIndex = selectedGroupIndex();
if (groupIndex === 0) {
// Admin group
const currentAdmin = config().adminGroupConfig;
setConfig("adminGroupConfig", {
...currentAdmin,
groupUsers: currentAdmin.groupUsers.filter((user) => user !== userName),
});
} else {
// Regular group
const actualIndex = groupIndex - 1;
const currentGroups = config().usersGroups;
const currentGroup = currentGroups[actualIndex];
const newGroups = [...currentGroups];
newGroups[actualIndex] = {
...currentGroup,
groupUsers: (currentGroup?.groupUsers ?? []).filter(
(user) => user !== userName,
),
};
setConfig("usersGroups", newGroups);
}
};
// Get all groups for selection
const getAllGroups = (): UserGroupConfig[] => {
const currentConfig = config();
return [currentConfig.adminGroupConfig, ...currentConfig.usersGroups];
};
// Get currently selected group
const getSelectedGroup = (): UserGroupConfig => {
const groups = getAllGroups();
return groups[selectedGroupIndex()] ?? config().adminGroupConfig;
};
/**
* Basic Configuration Tab
*/
const BasicTab = () => {
return div(
{ class: "flex flex-col" },
label({}, "Detect Interval (ms):"),
input({
type: "text",
value: () => config().detectInterval?.toString() ?? "",
onInput: (value) => {
const num = validateNumber(value);
if (num !== null) setConfig("detectInterval", num);
},
}),
label({}, "Watch Interval (ms):"),
input({
type: "text",
value: () => config().watchInterval?.toString() ?? "",
onInput: (value) => {
const num = validateNumber(value);
if (num !== null) setConfig("watchInterval", num);
},
}),
label({}, "Notice Times:"),
input({
type: "text",
value: () => config().noticeTimes?.toString() ?? "",
onInput: (value) => {
const num = validateNumber(value);
if (num !== null) setConfig("noticeTimes", num);
},
}),
label({}, "Detect Range:"),
input({
type: "text",
value: () => config().detectRange?.toString() ?? "",
onInput: (value) => {
const num = validateNumber(value);
if (num !== null) setConfig("detectRange", num);
},
}),
label({}, "Is Warn:"),
input({
type: "checkbox",
checked: () => config().isWarn ?? false,
onChange: (checked) => setConfig("isWarn", checked),
}),
);
};
/**
* Groups Configuration Tab
*/
const GroupsTab = () => {
const groups = getAllGroups();
const selectedGroup = getSelectedGroup();
return div(
{ class: "flex flex-row" },
// Left side - Groups list
div(
{ class: "flex flex-col" },
label({}, "Groups:"),
For({ each: () => groups }, (group, index) =>
button(
{
class:
selectedGroupIndex() === index() ? "bg-blue text-white" : "",
onClick: () => setSelectedGroupIndex(index()),
},
group.groupName,
),
),
),
// Right side - Group details
div(
{ class: "flex flex-col ml-2" },
label({}, () => `Group: ${selectedGroup.groupName}`),
label({}, "Is Allowed:"),
input({
type: "checkbox",
checked: () => selectedGroup.isAllowed,
onChange: (checked) => {
const groupIndex = selectedGroupIndex();
if (groupIndex === 0) {
const currentAdmin = config().adminGroupConfig;
setConfig("adminGroupConfig", {
...currentAdmin,
isAllowed: checked,
});
} else {
const actualIndex = groupIndex - 1;
const currentGroups = config().usersGroups;
const currentGroup = currentGroups[actualIndex];
const newGroups = [...currentGroups];
newGroups[actualIndex] = {
...currentGroup,
isAllowed: checked,
};
setConfig("usersGroups", newGroups);
}
},
}),
label({}, "Is Notice:"),
input({
type: "checkbox",
checked: () => selectedGroup.isNotice,
onChange: (checked) => {
const groupIndex = selectedGroupIndex();
if (groupIndex === 0) {
const currentAdmin = config().adminGroupConfig;
setConfig("adminGroupConfig", {
...currentAdmin,
isNotice: checked,
});
} else {
const actualIndex = groupIndex - 1;
const currentGroups = config().usersGroups;
const currentGroup = currentGroups[actualIndex];
const newGroups = [...currentGroups];
newGroups[actualIndex] = {
...currentGroup,
isNotice: checked,
};
setConfig("usersGroups", newGroups);
}
},
}),
label({}, "Group Users:"),
// User management
div(
{ class: "flex flex-row" },
input({
type: "text",
value: newUserName,
onInput: setNewUserName,
placeholder: "Enter username",
}),
button({ onClick: addUser }, "Add"),
),
// Users list
For({ each: () => selectedGroup.groupUsers ?? [] }, (user) =>
div(
{ class: "flex flex-row items-center" },
label({}, user),
button(
{
class: "ml-1 bg-red text-white",
onClick: () => removeUser(user),
},
"Remove",
),
),
),
),
);
};
/**
* Toast Configuration Tab Factory
*/
const createToastTab = (
toastType: "welcomeToastConfig" | "warnToastConfig" | "noticeToastConfig",
) => {
return () => {
const toastConfig = config()[toastType];
return div(
{ class: "flex flex-col" },
label({}, "Title (JSON):"),
input({
type: "text",
value: () => textutils.serialiseJSON(toastConfig?.title) ?? "",
onInput: (value) => {
try {
const parsed = textutils.unserialiseJSON(value);
if (parsed != undefined && typeof parsed === "object") {
const currentConfig = config();
const currentToast = currentConfig[toastType];
setConfig(toastType, {
...currentToast,
title: parsed as MinecraftTextComponent,
});
}
} catch {
// Invalid JSON, ignore
}
},
}),
label({}, "Message (JSON):"),
input({
type: "text",
value: () => textutils.serialiseJSON(toastConfig?.msg) ?? "",
onInput: (value) => {
try {
const parsed = textutils.unserialiseJSON(value);
if (parsed != undefined && typeof parsed === "object") {
const currentConfig = config();
const currentToast = currentConfig[toastType];
setConfig(toastType, {
...currentToast,
msg: parsed as MinecraftTextComponent,
});
}
} catch {
// Invalid JSON, ignore
}
},
}),
label({}, "Prefix:"),
input({
type: "text",
value: () => toastConfig?.prefix ?? "",
onInput: (value) => {
const currentConfig = config();
const currentToast = currentConfig[toastType];
setConfig(toastType, { ...currentToast, prefix: value });
},
}),
label({}, "Brackets:"),
input({
type: "text",
value: () => toastConfig?.brackets ?? "",
onInput: (value) => {
const currentConfig = config();
const currentToast = currentConfig[toastType];
setConfig(toastType, { ...currentToast, brackets: value });
},
}),
label({}, "Bracket Color:"),
input({
type: "text",
value: () => toastConfig?.bracketColor ?? "",
onInput: (value) => {
const currentConfig = config();
const currentToast = currentConfig[toastType];
setConfig(toastType, { ...currentToast, bracketColor: value });
},
}),
);
};
};
// Create toast tab components
const WelcomeToastTab = createToastTab("welcomeToastConfig");
const WarnToastTab = createToastTab("warnToastConfig");
const NoticeToastTab = createToastTab("noticeToastConfig");
/**
* Error Dialog
*/
const ErrorDialog = () => {
return Show(
{ when: () => errorState().show },
div(
{
class:
"fixed top-1/4 left-1/4 right-1/4 bottom-1/4 bg-red text-white border",
},
div(
{ class: "flex flex-col p-2" },
label({}, () => errorState().message),
button(
{
class: "mt-2 bg-white text-black",
onClick: hideError,
},
"OK",
),
),
),
);
};
/**
* Tab Content Renderer
*/
const TabContent = () => {
const tab = currentTab();
if (tab === TABS.BASIC) return BasicTab();
if (tab === TABS.GROUPS) return GroupsTab();
if (tab === TABS.WELCOME_TOAST) return WelcomeToastTab();
if (tab === TABS.WARN_TOAST) return WarnToastTab();
if (tab === TABS.NOTICE_TOAST) return NoticeToastTab();
return BasicTab(); // fallback
};
/**
* Main UI Layout
*/
return div(
{ class: "flex flex-col h-full" },
// Header
h1("Access Control Configuration"),
// Tab bar
div(
{ class: "flex flex-row" },
For({ each: () => tabNames }, (tabName, index) =>
button(
{
class: currentTab() === index() ? "bg-blue text-white" : "",
onClick: () => setCurrentTab(index() as TabIndex),
},
tabName,
),
),
),
// Content area
div({ class: "flex-1 p-2" }, TabContent()),
// Action buttons
div(
{ class: "flex flex-row justify-center p-2" },
button(
{
class: "bg-green text-white mr-2",
onClick: handleSave,
},
"Save",
),
button(
{
class: "bg-gray text-white",
onClick: () => {
// Close TUI - this will be handled by the application framework
error("TUI_CLOSE");
},
},
"Close",
),
),
// Error dialog overlay
ErrorDialog(),
);
};
/**
* Launch the Access Control TUI
*/
export function launchAccessControlTUI(): void {
try {
render(AccessControlTUI);
} catch (e) {
if (e === "TUI_CLOSE" || e === "Terminated") {
// Normal exit
return;
} else {
print("Error in Access Control TUI:");
printError(e);
}
}
}
// Export the main component for external use
export { AccessControlTUI };