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 "), { 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"); });