urit/docs/sysop-guide.js
2026-05-02 21:11:50 -04:00

895 lines
32 KiB
JavaScript

const fs = require("fs");
const {
Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell,
Header, Footer, AlignmentType, LevelFormat, HeadingLevel,
BorderStyle, WidthType, ShadingType, PageNumber, PageBreak,
TabStopType, TabStopPosition
} = require("docx");
// --- Color palette ---
const CLR = {
title: "1A5276",
heading: "1A5276",
accent: "2E75B6",
dimText: "555555",
tblHead: "D6EAF8",
tblAlt: "F2F8FC",
border: "B0C4DE",
black: "000000",
white: "FFFFFF",
};
const border = { style: BorderStyle.SINGLE, size: 1, color: CLR.border };
const borders = { top: border, bottom: border, left: border, right: border };
const cellMargins = { top: 60, bottom: 60, left: 100, right: 100 };
const noBorders = {
top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
};
// --- Helpers ---
function h1(text) {
return new Paragraph({
heading: HeadingLevel.HEADING_1,
children: [new TextRun(text)],
spacing: { before: 360, after: 200 },
});
}
function h2(text) {
return new Paragraph({
heading: HeadingLevel.HEADING_2,
children: [new TextRun(text)],
spacing: { before: 280, after: 160 },
});
}
function h3(text) {
return new Paragraph({
heading: HeadingLevel.HEADING_3,
children: [new TextRun(text)],
spacing: { before: 200, after: 120 },
});
}
function para(text, opts = {}) {
const runs = [];
if (typeof text === "string") {
runs.push(new TextRun({ text, ...opts }));
} else {
// Array of TextRun configs
for (const t of text) {
runs.push(new TextRun(t));
}
}
return new Paragraph({
children: runs,
spacing: { after: 140 },
});
}
function bold(text) {
return { text, bold: true };
}
function code(text) {
return { text, font: "Courier New", size: 20, color: "333333" };
}
function codeBlock(lines) {
return lines.map(line =>
new Paragraph({
children: [new TextRun({ text: line || " ", font: "Courier New", size: 19, color: "333333" })],
spacing: { after: 20 },
indent: { left: 400 },
shading: { type: ShadingType.CLEAR, fill: "F5F5F5" },
})
);
}
function bullet(text, opts = {}) {
const runs = [];
if (typeof text === "string") {
runs.push(new TextRun(text));
} else {
for (const t of text) runs.push(new TextRun(t));
}
return new Paragraph({
numbering: { reference: "bullets", level: opts.level || 0 },
children: runs,
spacing: { after: 80 },
});
}
function makeRow(cells, opts = {}) {
return new TableRow({
children: cells.map((text, i) => {
const widths = opts.widths || [4680, 4680];
return new TableCell({
borders,
width: { size: widths[i], type: WidthType.DXA },
margins: cellMargins,
shading: opts.shading ? { fill: opts.shading, type: ShadingType.CLEAR } : undefined,
children: [
new Paragraph({
children: [new TextRun({
text: String(text),
bold: opts.header || false,
font: "Arial",
size: opts.header ? 21 : 20,
color: opts.header ? CLR.heading : CLR.black,
})],
}),
],
});
}),
});
}
function makeTable(headers, rows, widths) {
const totalWidth = widths.reduce((a, b) => a + b, 0);
return new Table({
width: { size: totalWidth, type: WidthType.DXA },
columnWidths: widths,
rows: [
makeRow(headers, { widths, header: true, shading: CLR.tblHead }),
...rows.map((row, i) =>
makeRow(row, { widths, shading: i % 2 === 1 ? CLR.tblAlt : undefined })
),
],
});
}
function spacer() {
return new Paragraph({ spacing: { after: 80 }, children: [] });
}
// --- Document content ---
const children = [];
// ==================== TITLE PAGE ====================
children.push(new Paragraph({ spacing: { before: 3000 }, children: [] }));
children.push(new Paragraph({
alignment: AlignmentType.CENTER,
children: [new TextRun({ text: "URIT BBS", size: 72, bold: true, color: CLR.title, font: "Arial" })],
}));
children.push(new Paragraph({
alignment: AlignmentType.CENTER,
spacing: { after: 400 },
children: [new TextRun({ text: "Sysop Guide", size: 48, color: CLR.accent, font: "Arial" })],
}));
children.push(new Paragraph({
alignment: AlignmentType.CENTER,
spacing: { after: 200 },
children: [new TextRun({ text: "Version 0.2.0", size: 24, color: CLR.dimText, font: "Arial" })],
}));
children.push(new Paragraph({
alignment: AlignmentType.CENTER,
children: [new TextRun({
text: "A modern reimplementation of T.A.G.-BBS (1986) in Go",
size: 22, color: CLR.dimText, font: "Arial", italics: true,
})],
}));
children.push(new Paragraph({ children: [new PageBreak()] }));
// ==================== 1. INTRODUCTION ====================
children.push(h1("Introduction"));
children.push(para(
"URIT is a multi-node BBS written in Go, inspired by T.A.G.-BBS (The Amiga Gazette BBS, 1986). " +
"It preserves the classic BBS experience\u2014ANSI screens, message boards, private mail, file libraries, " +
"bulletins\u2014while replacing the Amiga serial port with telnet, flat files with SQLite, and ZMODEM " +
"with HTTP file transfers. Features that the single-node original never had, such as multi-node " +
"support and inter-node chat, are included."
));
children.push(para(
"This guide covers everything a sysop needs to install, configure, and manage a URIT BBS. It assumes " +
"familiarity with BBS concepts (nodes, security levels, message boards, file areas) but not with " +
"URIT or TAG specifically."
));
// ==================== 2. BUILDING AND INSTALLING ====================
children.push(h1("Building and Installing"));
children.push(para("URIT is a single Go binary with no runtime dependencies beyond SQLite (bundled via CGo). You need:"));
children.push(bullet([bold("Go 1.22+"), { text: " \u2014 " }, code("go version"), { text: " to verify" }]));
children.push(bullet([bold("GCC"), { text: " \u2014 required by the SQLite CGo binding (gcc or musl-gcc)" }]));
children.push(spacer());
children.push(para("Clone and build:"));
children.push(...codeBlock([
"git clone https://github.com/urit/urit.git",
"cd urit",
"go build -o urit ./cmd/urit/",
]));
children.push(para(
"This produces a single binary. Dependencies are vendored, so no network access is needed during the build. " +
"Copy the binary wherever you like; all runtime paths are configurable."
));
// ==================== 3. QUICK START ====================
children.push(h1("Quick Start"));
children.push(para("Initialize a new BBS instance and start it:"));
children.push(...codeBlock([
"./urit init",
"./urit",
]));
children.push(para([
{ text: "The " }, code("init"), { text: " command walks you through naming the BBS and creating the sysop account. " +
"It creates a config.toml, a data directory with the SQLite database, seed content (boards, a bulletin, " +
"a file library), and a screens directory with starter ANSI files." }
]));
children.push(para([
{ text: "Once running, connect via telnet on port 2323 (" }, code("telnet localhost 2323"),
{ text: ") and visit " }, code("http://localhost:8080"),
{ text: " in a browser to see the landing page." }
]));
// ==================== 4. COMMAND-LINE USAGE ====================
children.push(h1("Command-Line Usage"));
children.push(para("URIT has two modes: server and initialization."));
children.push(spacer());
children.push(makeTable(
["Command", "Description"],
[
["urit", "Start the BBS server"],
["urit init", "Initialize a new BBS instance"],
["urit version", "Print version and exit"],
["urit help", "Show help"],
],
[3000, 6360],
));
children.push(spacer());
children.push(para([
{ text: "Both " }, code("urit"), { text: " and " }, code("urit init"),
{ text: " accept " }, code("-config <path>"),
{ text: " to specify a configuration file (default: config.toml in the current directory). " +
"The init command also accepts " }, code("-force"), { text: " to overwrite an existing database." },
]));
// ==================== 5. CONFIGURATION ====================
children.push(h1("Configuration"));
children.push(para(
"URIT uses a single TOML file for all configuration. A default config.toml is created during " +
"initialization. Below is a reference of every section."
));
// --- [system] ---
children.push(h2("[system]"));
children.push(makeTable(
["Key", "Default", "Description"],
[
["name", '"URIT BBS"', "BBS name shown on banners and the HTTP landing page"],
["sysop", '"Sysop"', "Sysop display name"],
["location", '"./data/"', "Base data directory"],
["screens", '"./screens/"', "Path to ANSI screen files"],
],
[2000, 2200, 5160],
));
// --- [telnet] ---
children.push(h2("[telnet]"));
children.push(makeTable(
["Key", "Default", "Description"],
[
["enabled", "true", "Enable or disable the telnet listener"],
["address", '":2323"', "Listen address and port"],
],
[2000, 2200, 5160],
));
// --- [http] ---
children.push(h2("[http]"));
children.push(makeTable(
["Key", "Default", "Description"],
[
["enabled", "true", "Enable or disable the HTTP file server"],
["address", '":8080"', "Listen address and port"],
],
[2000, 2200, 5160],
));
children.push(para(
"The HTTP server provides the system landing page, file library browsing, file uploads and downloads, " +
"and a health-check endpoint at /health."
));
// --- [storage] ---
children.push(h2("[storage]"));
children.push(makeTable(
["Key", "Default", "Description"],
[
["driver", '"sqlite"', "Storage backend (only sqlite is supported)"],
["sqlite_path", '"./data/urit.db"', "Path to the SQLite database file"],
],
[2000, 2800, 4560],
));
// --- [users] ---
children.push(h2("[users]"));
children.push(makeTable(
["Key", "Default", "Description"],
[
["max_accounts", "500", "Maximum registered accounts"],
["guest_time_limit", "1800", "Guest session time limit in seconds (30 min)"],
["new_time_limit", "3600", "New user session time limit (60 min)"],
["valid_time_limit", "7200", "Validated user time limit (120 min)"],
],
[2600, 1600, 5160],
));
children.push(spacer());
children.push(para(
"Time limits are enforced per calendar day. If a user reconnects within 12 hours, their remaining " +
"time carries over from the previous session. After 12 hours, the timer resets. The sysop account " +
"(SecStatus 255) always gets a full reset on every login."
));
// --- [users.*_security] ---
children.push(h2("[users.guest_security] / [users.new_security] / [users.valid_security]"));
children.push(makeTable(
["Key", "Description"],
[
["status", "SecStatus tier assigned to users in this class"],
["board", "SecBoard level assigned to users in this class"],
["library", "SecLibrary level assigned to users in this class"],
["bulletin", "SecBulletin level assigned to users in this class"],
],
[2000, 7360],
));
children.push(spacer());
children.push(para(
"These define the default security levels for each user class. When someone registers, they receive " +
"the new_security values. When the sysop validates them, they receive the valid_security values. " +
"See the Security Model section for details on what the levels mean."
));
// --- [logging] ---
children.push(h2("[logging]"));
children.push(makeTable(
["Key", "Default", "Description"],
[
["level", '"info"', "Log verbosity"],
["file", '""', "Log file path (empty means stdout)"],
],
[2000, 2200, 5160],
));
// ==================== 6. SECURITY MODEL ====================
children.push(h1("Security Model"));
children.push(para(
"URIT uses a four-axis security system inherited from the original TAG-BBS. Every user has four " +
"independent security levels that control what they can access."
));
children.push(h2("SecStatus \u2014 Account Tier"));
children.push(para(
"This is the overall account classification. It determines which main menu commands are available " +
"and gates access to the sysop menu."
));
children.push(spacer());
children.push(makeTable(
["Range", "Label", "Description"],
[
["0", "Guest", "Unauthenticated browser; limited to public commands"],
["1", "New", "Registered but awaiting sysop validation"],
["2\u201399", "Valid", "Normal validated user"],
["100\u2013149", "BoardOp", "Can moderate message boards"],
["150\u2013254", "LibOp", "Can manage file libraries"],
["255", "Sysop", "Full administrative access"],
],
[1600, 1400, 6360],
));
children.push(h2("SecBoard, SecLibrary, SecBulletin"));
children.push(para(
"These are numeric levels (typically 0\u201310) checked against the Low/High access range on each board, " +
"library, or bulletin. For example, a board with ReadLow=2 and ReadHigh=10 is visible to any user " +
"whose SecBoard is between 2 and 10 inclusive."
));
children.push(para(
"This lets you create tiered content: public boards at level 0, member boards at level 2, staff " +
"boards at level 5, and so on. The same pattern applies to libraries (with separate ranges for " +
"upload and download access) and bulletins."
));
children.push(para(
"The sysop can edit all four security levels per user from the sysop menu."
));
// ==================== 7. SCREEN FILES ====================
children.push(h1("Screen Files"));
children.push(para(
"Screen files are ANSI art text files displayed at various points during a session. They live in " +
"the directory specified by system.screens (default: ./screens/). Replace them with your own " +
"ANSI art to customize the look and feel of your BBS."
));
children.push(spacer());
children.push(makeTable(
["File", "When Displayed"],
[
["welcome.ans", "Before the login prompt (pre-auth banner)"],
["login.ans", "Alongside the login prompt"],
["guest.ans", "After guest login"],
["newuser.ans", "After first-time registration"],
["logon.ans", "After successful login for returning users"],
["notime.ans", "When a user has no remaining time (then disconnected)"],
["mainmenu.ans", "Above the main menu (blank = show generated command list)"],
["help.ans", "For the [?] help command (blank = generated list)"],
["join.ans", "Before the account creation prompts"],
["joined.ans", "After successful account creation"],
["goodbye.ans", "At logoff"],
],
[2400, 6960],
));
children.push(spacer());
children.push(para(
"Bulletin display files are stored wherever the bulletin\u2019s FilePath points. These are managed " +
"through the sysop bulletin menu."
));
children.push(para(
"Screen files use standard ANSI escape codes. Classic BBS ANSI editors such as PabloDraw, Moebius, " +
"and SyncDraw produce compatible output. Keep line widths to 80 columns for the best terminal experience."
));
// ==================== 8. THE SYSOP MENU ====================
children.push(h1("The Sysop Menu"));
children.push(para([
{ text: "Log in as the sysop account (created during " }, code("urit init"),
{ text: ") and press " }, bold("E"), { text: " at the main menu to enter the sysop management area." },
]));
children.push(spacer());
children.push(makeTable(
["Key", "Command", "Description"],
[
["L", "List all users", "Paginated user listing with status and last login"],
["N", "New/unvalidated", "Shows only users with SecStatus=1 awaiting validation"],
["V", "View/edit user", "Full account editor (security, stats, password, active flag)"],
["B", "Board management", "Create, edit, delete message boards"],
["U", "Bulletin management", "Create, edit, delete bulletins and their display files"],
["F", "File library mgmt", "Create, edit, delete file libraries with access ranges"],
["C", "Call log", "Last 30 connection events with timestamps, IPs, and details"],
["X", "Force disconnect", "Kick a node by number (sends courtesy message first)"],
["Q", "Return", "Back to the main menu"],
],
[800, 2800, 5760],
));
children.push(h2("Validating New Users"));
children.push(para(
"When someone registers, they receive SecStatus=1 (New). They can use the BBS but with limited " +
"access defined by the new_security configuration. To validate a user, navigate to the sysop menu " +
"and press V to view/edit their account, then press 2 to validate. This promotes the user to " +
"the valid_security levels defined in your config. You can also manually adjust their security " +
"levels with keys F through I."
));
children.push(h2("User Account Editor"));
children.push(para(
"From the sysop menu, press V and enter a user ID. The editor displays all account fields and " +
"accepts single-key commands."
));
children.push(spacer());
children.push(makeTable(
["Key", "Action"],
[
["1", "Save changes"],
["ESC", "Cancel (discard changes)"],
["2", "Validate user (set to valid security levels)"],
["3", "Toggle active/deactivated"],
["D", "Permanently delete user"],
["A", "Change username"],
["B", "Reset password"],
["C", "Edit comments"],
["F", "Set SecStatus"],
["G", "Set SecBoard"],
["H", "Set SecLibrary"],
["I", "Set SecBulletin"],
["J\u2013N", "Edit stats (messages posted, mail sent/received, uploads, downloads)"],
["Q", "Set time limit"],
["R", "Set time used"],
],
[1200, 8160],
));
children.push(h2("Board Management"));
children.push(para(
"The board management submenu (sysop menu \u2192 B) lets you list, create, edit, and delete " +
"message boards. Each board has a name and four access boundaries: ReadLow, ReadHigh, WriteLow, " +
"and WriteHigh, all checked against the user\u2019s SecBoard level."
));
children.push(para(
"A board with ReadLow=0 and ReadHigh=255 is visible to everyone. Setting WriteLow=2 means guests " +
"and new users can read but not post. MaxPosts sets the board\u2019s capacity; when reached, the " +
"oldest messages are purged as new ones arrive."
));
children.push(h2("Library Management"));
children.push(para(
"The library management submenu (sysop menu \u2192 F) manages file libraries. Libraries have " +
"separate upload and download access ranges, both checked against the user\u2019s SecLibrary level."
));
children.push(para([
{ text: "A library with DownloadLow=0 is publicly browsable via the HTTP server without login. " +
"Set DownloadLow higher to restrict access to authenticated users. UploadLow and UploadHigh " +
"control who can upload files. MaxFiles sets the capacity limit. " },
bold("FilePath"),
{ text: " is the directory on disk where uploaded files are stored; the server creates it " +
"automatically on the first upload." },
]));
children.push(h2("Bulletin Management"));
children.push(para(
"Bulletins (sysop menu \u2192 U) are ANSI text files displayed to users from the bulletin menu. " +
"Each bulletin has a name, a file path pointing to the display file on disk, and a " +
"ReadLow/ReadHigh range checked against SecBulletin."
));
// ==================== 9. HTTP FILE SERVER ====================
children.push(h1("HTTP File Server"));
children.push(para(
"The HTTP server (port 8080 by default) is the modern replacement for ZMODEM and other file " +
"transfer protocols. It provides library browsing, file downloads, and file uploads through " +
"a browser interface styled to match the BBS\u2019s terminal aesthetic."
));
children.push(h2("Routes"));
children.push(makeTable(
["Path", "Purpose"],
[
["/", "Landing page with system name, stats, and telnet address"],
["/health", "JSON health check for monitoring"],
["/login", "Browser-based login (BBS username/password)"],
["/logout", "Clear session"],
["/libraries", "List of accessible file libraries"],
["/libraries/{id}", "File listing for a specific library"],
["/libraries/{id}/files/{n}", "File download"],
["/libraries/{id}/upload", "File upload form"],
],
[3800, 5560],
));
children.push(h2("Authentication"));
children.push(para("The HTTP server supports two authentication methods."));
children.push(para([
bold("Browser login"),
{ text: " \u2014 Visit /login and enter your BBS username and password. This creates a cookie-based " +
"session lasting 24 hours. The same bcrypt password check as the telnet side is used." },
]));
children.push(para([
bold("Telnet tokens"),
{ text: " \u2014 From the BBS main menu, press D to generate a one-time URL. Open it in your browser " +
"to get authenticated with your BBS security levels. Tokens expire after one hour. This is " +
"convenient for telnet users who want to download files without re-entering their password." },
]));
children.push(para(
"Anonymous visitors see only public libraries (DownloadLow=0). Authenticated users see all " +
"libraries matching their SecLibrary level."
));
children.push(h2("File Uploads"));
children.push(para(
"Authenticated users can upload to libraries where their SecLibrary falls within the library\u2019s " +
"UploadLow\u2013UploadHigh range. The upload form is accessible from the file listing page. " +
"Files are limited to 50 MB. Duplicate filenames within the same library are rejected. The " +
"library\u2019s directory on disk is created automatically if it does not exist."
));
// ==================== 10. INTER-NODE CHAT ====================
children.push(h1("Inter-Node Chat"));
children.push(para([
{ text: "URIT supports real-time communication between connected users. Press " },
bold("C"),
{ text: " at the main menu to enter the chat system. Three modes are available." },
]));
children.push(para([
bold("Page"),
{ text: " \u2014 Send a one-line message to another node. The message appears on their terminal " +
"immediately, regardless of what they are doing. No chat mode is required on either end." },
]));
children.push(para([
bold("Chat"),
{ text: " \u2014 Enter real-time line-by-line messaging with another node. When you start a chat, " +
"the target user receives a notification. If they also press C and select your node, the " +
"channels link and messages flow bidirectionally. Type /quit to exit. If either user " +
"disconnects, the partner is notified." },
]));
children.push(para([
bold("Broadcast"),
{ text: " (sysop only) \u2014 Send a message to every connected node at once. Useful for " +
"maintenance announcements or server shutdown warnings." },
]));
// ==================== 11. MAIN MENU COMMANDS ====================
children.push(h1("Main Menu Commands"));
children.push(para(
"The following commands are available from the main menu. Guest users see only commands marked " +
"as guest-accessible. Registered users see all non-sysop commands."
));
children.push(spacer());
children.push(makeTable(
["Key", "Name", "Description"],
[
["I", "Info", "System information"],
["S", "Stats", "System statistics (calls, messages, time)"],
["T", "Time", "Session time remaining"],
["W", "Who", "Who\u2019s online (node list)"],
["A", "Account", "View your account details"],
["P", "Mail", "Private mail system"],
["M", "Messages", "Message boards"],
["B", "Bulletins", "Bulletin listings"],
["L", "Library", "File library browser (telnet side)"],
["D", "Download", "Generate a web download token URL"],
["C", "Chat", "Page/chat with other users"],
["U", "Users", "User listings"],
["F", "Feedback", "Send a note to the sysop (delivered as mail)"],
["J", "Join", "Create a permanent account (guest only)"],
["?", "Help", "Command list"],
["G", "Goodbye", "Log off"],
["E", "Sysop", "Sysop management menu (sysop only)"],
],
[800, 1600, 6960],
));
// ==================== 12. STATISTICS AND CALL LOG ====================
children.push(h1("Statistics and Call Log"));
children.push(para("URIT tracks system activity in two ways."));
children.push(para([
bold("Stats counters"),
{ text: " are atomic key-value counters stored in the database. The system tracks: total_calls, " +
"guest_calls, new_calls, valid_calls, new_accounts, messages_posted, mail_sent, total_time_secs, " +
"files_downloaded, and files_uploaded. Users can view a summary from the main menu with S." },
]));
children.push(para([
bold("The call log"),
{ text: " records timestamped events (login, logoff, kicked) with the user name, node number, " +
"remote IP, and detail text. The sysop call log (sysop menu \u2192 C) shows the last 30 events " +
"with full detail. The user-facing stats screen shows the last 10 login/logoff events without " +
"IP addresses." },
]));
// ==================== 13. SESSION LIFECYCLE ====================
children.push(h1("Session Lifecycle"));
children.push(para(
"Understanding the session flow helps when troubleshooting or customizing screens. The following " +
"is the sequence from connection to logoff."
));
children.push(spacer());
children.push(makeTable(
["Step", "Action", "Screen File"],
[
["1", "Telnet connection; node allocated", "welcome.ans"],
["2", "Login prompt; username/password checked (3 attempts, then guest)", "login.ans"],
["3a", "New user: registration flow", "newuser.ans"],
["3b", "Guest login", "guest.ans"],
["3c", "Returning user login", "logon.ans"],
["4", "Time check: if >12 hrs since last login, reset; otherwise carry over", "(none)"],
["4x", "No time remaining", "notime.ans"],
["5", "Last caller display, unread mail check, validation notice", "(none)"],
["6", "Main menu loop", "mainmenu.ans"],
["7", "Logoff; time/stats saved; node freed", "goodbye.ans"],
],
[900, 5660, 2800],
));
children.push(spacer());
children.push(para(
"The logoff handler runs in a deferred function, so stats and time are saved even if the " +
"connection drops unexpectedly."
));
// ==================== 14. DATABASE ====================
children.push(h1("Database"));
children.push(para(
"All persistent state lives in a single SQLite database (default: ./data/urit.db). The schema " +
"is created automatically on first run."
));
children.push(spacer());
children.push(makeTable(
["Table", "Contents"],
[
["users", "User accounts (name, password hash, security levels, stats)"],
["boards", "Message boards (name, topic, access ranges)"],
["messages", "Board messages (author, subject, body, timestamps)"],
["mail", "Private mail between users"],
["libraries", "File libraries (name, path, access ranges, capacity)"],
["library_files", "File metadata (name, size, uploader, download count)"],
["bulletins", "Bulletin definitions (name, display file path)"],
["call_log", "Timestamped connection events"],
["stats", "Key-value counters"],
["web_sessions", "HTTP authentication sessions"],
],
[2200, 7160],
));
children.push(spacer());
children.push(para([
{ text: "The database can be backed up by copying the file while the server is stopped. For a " +
"running system, use " },
code('sqlite3 urit.db ".backup backup.db"'),
{ text: " which is safe for concurrent reads." },
]));
// ==================== 15. DEPLOYMENT TIPS ====================
children.push(h1("Deployment Tips"));
children.push(h2("Reverse Proxy"));
children.push(para(
"Put nginx or Caddy in front of the HTTP server for TLS. The telnet server does not support TLS " +
"natively; consider a stunnel or haproxy wrapper if you need encrypted telnet connections."
));
children.push(h2("Systemd"));
children.push(para(
"Create a service unit that runs the binary with your config file. Set Restart=on-failure for " +
"automatic recovery. Use a dedicated system user for isolation. A minimal unit file:"
));
children.push(...codeBlock([
"[Unit]",
"Description=URIT BBS",
"After=network.target",
"",
"[Service]",
"Type=simple",
"User=urit",
"WorkingDirectory=/opt/urit",
"ExecStart=/opt/urit/urit -config /opt/urit/config.toml",
"Restart=on-failure",
"",
"[Install]",
"WantedBy=multi-user.target",
]));
children.push(h2("Backups"));
children.push(para(
"Back up two things: the SQLite database and the screens directory. File libraries are stored on " +
"disk at the paths configured per library, so include those directories as well. " +
"The config.toml should be kept in version control or backed up separately."
));
children.push(h2("Monitoring"));
children.push(para([
{ text: "The " }, code("/health"),
{ text: " endpoint returns JSON with the system name and current timestamp. " +
"Point your monitoring tool at it for basic availability checks." },
]));
// ==================== 16. APPENDIX: CONFIG DEFAULTS ====================
children.push(h1("Appendix: Default Configuration"));
children.push(para("For reference, here is the complete default config.toml produced by initialization:"));
children.push(...codeBlock([
'[system]',
'name = "URIT BBS"',
'sysop = "Sysop"',
'location = "./data/"',
'screens = "./screens/"',
'',
'[telnet]',
'enabled = true',
'address = ":2323"',
'',
'[http]',
'enabled = true',
'address = ":8080"',
'',
'[storage]',
'driver = "sqlite"',
'sqlite_path = "./data/urit.db"',
'',
'[users]',
'max_accounts = 500',
'guest_time_limit = 1800',
'new_time_limit = 3600',
'valid_time_limit = 7200',
'',
'[users.guest_security]',
'status = 0',
'board = 0',
'library = 0',
'bulletin = 0',
'',
'[users.new_security]',
'status = 1',
'board = 1',
'library = 1',
'bulletin = 1',
'',
'[users.valid_security]',
'status = 2',
'board = 2',
'library = 2',
'bulletin = 2',
'',
'[logging]',
'level = "info"',
'file = ""',
]));
// ==================== BUILD DOCUMENT ====================
const doc = new Document({
styles: {
default: {
document: {
run: { font: "Arial", size: 22, color: CLR.black },
},
},
paragraphStyles: [
{
id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 36, bold: true, font: "Arial", color: CLR.title },
paragraph: {
spacing: { before: 360, after: 200 },
outlineLevel: 0,
border: { bottom: { style: BorderStyle.SINGLE, size: 4, color: CLR.accent, space: 4 } },
},
},
{
id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 28, bold: true, font: "Arial", color: CLR.heading },
paragraph: { spacing: { before: 240, after: 160 }, outlineLevel: 1 },
},
{
id: "Heading3", name: "Heading 3", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 24, bold: true, font: "Arial", color: CLR.heading },
paragraph: { spacing: { before: 200, after: 120 }, outlineLevel: 2 },
},
],
},
numbering: {
config: [
{
reference: "bullets",
levels: [
{
level: 0, format: LevelFormat.BULLET, text: "\u2022",
alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 720, hanging: 360 } } },
},
{
level: 1, format: LevelFormat.BULLET, text: "\u2013",
alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 1440, hanging: 360 } } },
},
],
},
],
},
sections: [
{
properties: {
page: {
size: { width: 12240, height: 15840 },
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 },
},
},
headers: {
default: new Header({
children: [
new Paragraph({
alignment: AlignmentType.RIGHT,
border: { bottom: { style: BorderStyle.SINGLE, size: 1, color: CLR.border, space: 4 } },
children: [
new TextRun({ text: "URIT BBS Sysop Guide", font: "Arial", size: 18, color: CLR.dimText }),
],
}),
],
}),
},
footers: {
default: new Footer({
children: [
new Paragraph({
alignment: AlignmentType.CENTER,
border: { top: { style: BorderStyle.SINGLE, size: 1, color: CLR.border, space: 4 } },
children: [
new TextRun({ text: "Page ", font: "Arial", size: 18, color: CLR.dimText }),
new TextRun({ children: [PageNumber.CURRENT], font: "Arial", size: 18, color: CLR.dimText }),
],
}),
],
}),
},
children,
},
],
});
Packer.toBuffer(doc).then(buffer => {
fs.writeFileSync("/mnt/user-data/outputs/URIT-BBS-Sysop-Guide.docx", buffer);
console.log("Done: URIT-BBS-Sysop-Guide.docx");
});