code.pablomurad.com

bbs-hammer

SSH BBS with forums, chat, door games, retro terminals and a weather service, written in Go

  1. 51c41a7
    feat(radio): web station browser, https fallback
    

    Pablo Murad

  2. 78a5a8d
    fix(almanac): keep the city name as typed
    

    Pablo Murad

  3. ea7ca52
    fix(radio): hide favorites hint for guests
    

    Pablo Murad

  4. 5cd1176
    fix(linecast): pass units via env, show quick failures
    

    Pablo Murad

  5. af0255b
    docs: bulletin, linecast and radio
    

    Pablo Murad

  6. 8353830
    feat(radio): dispatcher tools
    

    Pablo Murad

  7. 3e76e46
    feat(radio): Hammer FM in bulletin, PETSCII radio
    

    Pablo Murad

  8. 1975ad8
    feat(chat): add /np
    

    Pablo Murad

  9. 6be1158
    feat: show radio and sky activity in presence
    

    Pablo Murad

  10. 186c1b3
    feat(radio): web player pages
    

    Pablo Murad

  11. 63c1fa1
    feat(radio): [o] radio menu and Hammer FM
    

    Pablo Murad

  12. f0ffb11
    feat(radio): Radio Browser client, ICY titles, favorites
    

    Pablo Murad

  13. 3c4f97c
    build(docker): add linecast to the games image
    

    Pablo Murad

  14. 2d31598
    feat(linecast): [k] sky & almanac menu
    

    Pablo Murad

  15. 4d0c1a2
    feat(linecast): config and launch args
    

    Pablo Murad

  16. 372227e
    feat(doors): forward window resizes to pty doors
    

    Pablo Murad

  17. da7486e
    feat(door): pty support
    

    Pablo Murad

  18. cc927b1
    feat: login bulletin and almanac
    

    Pablo Murad

  19. 1a6bae0
    fix(doors): request the pinned host key type from DoorParty
    

    Pablo Murad

  20. 6055b56
    feat(doors): "menu" door opens the server menu
    

    Pablo Murad

  21. 8bb8b1a
    docs: doors, MRC, echomail, rlogin/FTP/PETSCII
    

    Pablo Murad

  22. efd8a0c
    build(docker): full target with DOSBox
    

    Pablo Murad

  23. d321a59
    feat: ANSI postcards
    

    Pablo Murad

  24. e5d7043
    test(bbs): fix wall/about flake
    

    Pablo Murad

  25. 7812963
    test(bbs): wait for the right badges screen
    

    Pablo Murad

  26. 21dfb32
    feat(bbs): badges
    

    Pablo Murad

  27. 306e7e5
    feat(bbs): /:live page
    

    Pablo Murad

  28. 6b69c6c
    feat: PETSCII ports for C64/C128
    

    Pablo Murad

  29. cf911a6
    feat: read-only anonymous FTP
    

    Pablo Murad

  30. 869187b
    feat(bbs): rlogin listener
    

    Pablo Murad

  31. 5eed667
    feat(bbs): DOVE-Net echomail over QWK
    

    Pablo Murad

  32. 8801d3f
    feat(qwk): QWK/REP packets and FTP client
    

    Pablo Murad

  33. 8db0138
    feat(bbs): fsxNet echomail over binkp
    

    Pablo Murad

  34. d098464
    style(ftn): gofmt
    

    Pablo Murad

  35. a0d8d34
    feat(ftn): packets, echomail and binkp
    

    Pablo Murad

  36. 429d2d8
    feat(forum): network fields for echomail
    

    Pablo Murad

  37. 936071e
    feat(bbs): MRC chat
    

    Pablo Murad

  38. 03e6015
    feat(bbs): native, DOS and remote doors
    

    Pablo Murad

  39. d5b5efc
    feat(door): drop files and socket doors
    

    Pablo Murad

  40. 75c838e
    fix: Lawn Mower URL, CRLF drop files
    

    Pablo Murad

  41. 43fa37d
    docs: forums, telnet, web terminal, CP437, nodes
    

    Pablo Murad

  42. 26ba449
    feat(bbs): node monitor for moderators
    

    Pablo Murad

  43. dc5f287
    feat(bbs): browser terminal
    

    Pablo Murad

  44. 86a0080
    feat(bbs): telnet listener
    

    Pablo Murad

  45. ceb926b
    feat(bbs): CP437 for SyncTERM and other BBS terminals
    

    Pablo Murad

  46. 83e410b
    feat(bbs): forums on the menu and the dispatcher
    

    Pablo Murad

  47. 756b56f
    feat(forum): message areas, topics and posts in SQLite
    

    Pablo Murad

  48. 666da39
    feat(community): arrival notices
    

    Pablo Murad

  49. b08d1bf
    feat(community): last callers
    

    Pablo Murad

  50. faf9ce0
    refactor(bbs): run sessions over any transport
    

    Pablo Murad

  51. dddad0a
    feat(chat): keep the chat in the database
    

    Pablo Murad

  52. aa2383b
    feat(db): open the BBS database in WAL mode with a busy timeout
    

    Pablo Murad

  53. 8278016
    fix(games): message for an empty games folder
    

    Pablo Murad

  54. 1cea366
    feat(games): games folder on the host
    

    Pablo Murad

  55. 8585ee9
    fix(fetchroms): retry on 429 and 5xx
    

    Pablo Murad

  56. e027a9d
    docs: NES games
    

    Pablo Murad

  57. b84d8cb
    build(docker): games target
    

    Pablo Murad

  58. b845aef
    fix(door): absolute drop file path
    

    Pablo Murad

  59. a2b5dfa
    feat(games): homebrew catalog and downloader
    

    Pablo Murad

  60. 6cc53f0
    feat(bbs): NES games through nesdoor
    

    Pablo Murad

  61. 68b2ca1
    feat(door): run doors over stdio
    

    Pablo Murad

  62. 1a0487c
    feat(http): the card is the default curl report
    
    The classic report is 125 columns wide, so in a normal terminal window
    every row wrapped and the frames fell apart. A request with no view now
    gets the card, 64 columns wide. The classic report stays one parameter
    away (?view=v1), and options only it understands (0-3, n, days=) or a PNG
    still select it. The BBS keeps the classic report when a tool call names
    no view.
    
    Along the way:
    - the card's title comes from the geocoder ("São Paulo, Brasil", "Tour
      Eiffel, France") instead of the lower-cased query or bare coordinates
    - the rain column is padded, so "9%" no longer shifts the wind column
    - in the browser each emoji takes exactly two cells, as in a terminal,
      so framed rows keep their right border in line
    - help page and README describe the new default
    

    Pablo Murad

  63. c55f513
    feat(http): curl reports dressed as the BBS
    
    Framed reports (v1, v2, v2d, v2n, card) now come out in Hammer's look: a
    "HAMMER" header, double-line frames in the retro palette, and a footer
    with the tagline and the command that opens the BBS, taken from the new
    brand block of the configuration (brand.ssh; empty drops it). ?F drops
    the footer, ?Q drops header and footer, ?T and ?d behave as before. The
    BBS itself renders directly and is not dressed.
    
    Terminals disagree about how wide an emoji is, which pushed the right
    border of card and v2 rows out of line. Those rows now move the cursor to
    the frame's last column (CSI n G) before drawing the border.
    
    Help pages name this host (brand.host) instead of the upstream one.
    
    Long forecast descriptions in v1 are cut at a word ("Patchy rain…"
    rather than "Patchy rain ne…"), and one that fits exactly no longer gets
    an ellipsis it does not need.
    
    README: independent status line, "How reports look", credits point to the
    new NOTICE, which keeps the attribution the Apache licence asks for.
    

    Pablo Murad

  64. 477370f
    refactor: Hammer stands on its own
    
    Removes what still spoke for the project Hammer grew out of:
    
    - translations: the upstream author's "Follow" footer and two unused
      strings that advertised the upstream service, in every language; his
      name from the translator credits (the volunteers stay)
    - web page: our title, no third-party social buttons or their scripts
    - translation page and bash helper: our names, no upstream contacts
    - sample config: /hammer paths instead of the upstream's
    - dead files: the SaltStack and systemd recipes for the upstream host, a
      lolcat wrapper with the upstream author's home path, and an orphan HTML
      page the static handler would still serve
    
    Also fixes a trailing comma in the crk translation metadata.
    

    Pablo Murad

  65. 5da9678
    style: centre the splash prompt and chat title, brighter retro hotkeys
    
    Found in screenshots of real sessions: the splash's "press any key" sat at
    column 0 under centred art, the chat's title and hint did not line up with
    the art above them, and the retro theme's dark blue hotkey brackets were
    nearly invisible on a dark background (now light blue).
    

    Pablo Murad

  66. 85ce72d
    feat(bbs): themes per session, ANSI art on screen, modem effect
    
    Every interactive session now picks its theme from the profile (or
    bbs.style.defaultTheme, retro90 by default) at the colour level its
    terminal reports, and the art shows up:
    
    - splash: the Hammer piece at modem speed (bbs.style.baud, 9600 by
      default), credited from its SAUCE record; any key finishes the drawing
    - wall, chat, weather, tools and settings: header art above the menu when
      the terminal is wide and tall enough
    - goodbye: NO CARRIER, then "see you"
    - narrower than 80 columns, or no colour: text titles, no art
    
    Input is now read by a goroutine per session and handed to nextByte over
    a channel, so a screen can check for a key without blocking; this is what
    lets a keypress cut the modem effect short.
    
    A status bar pins "nick · N online · theme · connected for" to the last
    row of menu screens. Settings gains theme and modem-effect entries,
    saved in the profile. Dumb terminals get the plain theme: ASCII boxes, no
    colour codes and no cursor movement.
    
    README gains a Look and feel section; deploy configs gain bbs.style;
    .gitattributes marks .ans files as binary.
    

    Pablo Murad

  67. 98a0bf5
    fix(profiles): saving a profile with a nickname lost every change
    
    Save tried an INSERT first and fell back to UPDATE on a unique-constraint
    error. Once profiles had nicknames, SQLite reported the nickname index
    before the primary key when re-saving an existing profile, Save read that
    as "nickname taken", and returned without updating. Units, language,
    cities and every other setting of a keyed user silently stopped being
    saved.
    
    Save now updates an existing row and inserts only new ones; a nickname
    conflict is reported only when another key really holds the name. A
    regression test re-saves a profile with its own nickname unchanged.
    
    The profiles table also gains theme and fx columns (same in-place
    migration as nick) for the look-and-feel settings that follow.
    

    Pablo Murad

  68. 178bc11
    feat(art): Hammer's own ANSI art
    
    Seven pieces drawn from scratch for the BBS, saved as real .ans files
    (CP437, iCE colours, SAUCE credited to hammer / hammer bbs) that open in
    PabloDraw and Moebius:
    
    - splash: HAMMER in steel-and-ice block letters with a drop shadow, a
      hammer struck by lightning, the tagline and art-pack shade bars
    - wall: THE WALL over a brick wall
    - chat: speech bubbles
    - weather: sun behind a rain cloud
    - tools: open-end spanner and screwdriver
    - setup: mixing-desk sliders
    - nocarrier: NO CARRIER over a modem front panel with CD gone dark
    
    They are drawn by tools/artgen (go generate ./internal/bbs) on a pixel
    layer at two pixels per cell (▀ ▄ █), with a chunky block font, gradients
    and shadows. artgen -png renders previews through the same loader the BBS
    uses. Third-party art can join them as plain .ans files, with the
    author's permission.
    

    Pablo Murad

  69. 46f86dc
    feat(ansiart): load and render classic ANSI art
    
    A package for .ans files as the art scene makes them:
    
    - SAUCE records are parsed (title, author, group, date, width, iCE flag)
      and stripped with their COMNT block and the ^Z marker.
    - CP437 is decoded to Unicode; files already in UTF-8 pass through.
    - The file is played on a virtual 80-column terminal (go-te, already a
      dependency), so cursor movement, wrapping, bold-as-bright and
      blink-as-bright-background land where the artist put them.
    - Render emits the art in any 16-colour palette at the terminal's colour
      level (truecolor, 256, 16 or none), leaving black backgrounds to the
      terminal, and Credit builds the attribution line from SAUCE.
    
    Canvas draws a grid and encodes it as a real .ans file (CP437, iCE colours,
    CRLF, SAUCE), which is how Hammer's own art is produced.
    

    Pablo Murad

  70. 8c4a426
    refactor(ui): themes instead of fixed colours
    
    The BBS palette lived in constants, so there could only ever be one look.
    A Theme now carries the colour roles, box glyphs, title decoration, block
    shadow, hotkey style, menu leader and the 16-colour palette ANSI art is
    drawn with:
    
    - retro90: VGA palette, double-line boxes with block shadows, blue
      brackets around hotkeys, dotted leaders in menus
    - modern: rounded boxes, a softer palette and gradient titles on
      truecolor terminals
    - plain: ASCII boxes and no escape sequences, for dumb terminals
    
    Colour is emitted at the level the terminal supports (truecolor, 256,
    16, none), detected from TERM and COLORTERM.
    
    Every screen in the bbs package now paints through the session's theme
    (st.th); menuEntry, settingsEntry, wallLine and colorNick became session
    methods. Sessions still use the modern theme at 256 colours, so nothing
    changes visibly yet; picking the theme comes later.
    

    Pablo Murad

  71. fe7d2b8
    feat(dispatch): who, wall and wall.post; document the community
    
    Scripts and agents can now see who is online (`who`), read the wall
    (`wall count=N`) and post to it (`wall.post text="..."`). Posting needs a
    key and honours the same cooldown and bans as the menu; a cooldown comes
    back as rate_limited with retry_after. The chat stays interactive only.
    
    README: a Community section (nicknames, who's online, wall, chat,
    moderation, anonymous read-only) and the bbs.community block in the
    configuration example. Runbook: how to make your key a moderator.
    

    Pablo Murad

  72. 08526cf
    feat(bbs): wall, who's online and live chat screens
    
    Main menu: the title shows how many people are online, the latest
    one-liners sit above the menu the way a BBS greets you with its wall, and
    two new entries open the wall [w] and the chat [c].
    
    Wall: the latest lines from everyone with their age and a colour per
    nickname; post (keys only, with the per-key cooldown), see who is online
    (where they are, for how long, how many connections), and, for
    moderators, delete a line by number.
    
    Chat: messages scroll above an input line that stays put. The session
    goroutine owns input and a receiver goroutine draws incoming messages;
    both write under the session's write lock and the receiver redraws the
    line being typed after each message. Long input scrolls sideways so one
    erase always clears it. Commands: /help /who /me /nick /quit, and for
    moderators /mute /kick /ban /unban. Anonymous visitors read but cannot
    talk. Ctrl+C leaves the room, not the BBS. `ssh -t host chat` opens the
    room directly.
    
    System events (joins, kicks, renames) are rendered in each reader's
    language. End-to-end tests drive several real SSH clients: messages
    crossing between people, escape sequences stripped, flood limit,
    read-only anonymous, moderator kick, wall cooldown and the online list.
    

    Pablo Murad

  73. e063948
    feat(bbs): nicknames and community settings
    
    Every key now has a nickname, unique regardless of case:
    
    - New keys get their SSH user name (ssh pablo@host -> pablo) when it is
      meaningful and free, otherwise guest-xxxx, widened until free.
    - Profiles saved before nicknames get one on their next login; the
      profiles table gains a nick column through an in-place migration.
    - Settings has a nickname entry with validation (2-16 characters,
      reserved names refused) and a friendly message when it is taken.
    - Anonymous sessions get a guest name that is never stored.
    
    The unique index is on the column with COLLATE NOCASE rather than an
    expression: godb's SQLite error parser panics on the message an
    expression index produces.
    
    A new bbs.community block configures moderators (key fingerprints), the
    wall (lines shown, length, cooldown) and the chat (length, rate). The
    server builds the presence tracker, wall and chat hub on the profiles
    database.
    

    Pablo Murad

  74. 3609e3a
    feat(community): presence, one-liner wall and chat hub
    
    A terminal-free package for the social side of the BBS:
    
    - CleanText strips escape sequences (CSI, OSC and the rest), C0/C1
      controls and bidi overrides from anything one user shows another, and
      cuts it to a width in terminal cells.
    - Nickname rules, reserved names and guest-xxxx fallbacks.
    - Presence: who is online, one entry per key, in memory.
    - Wall: the last one-liners from everyone, a per-key cooldown and pruning,
      stored in the BBS's SQLite database.
    - Hub: a chat room with a 100-message in-memory history, per-key rate
      limit, a bounded queue per member so a slow reader never blocks the room,
      read-only anonymous members, and moderation (mute, kick, ban, unban) with
      bans persisted and honoured by the wall too.
    

    Pablo Murad

  75. 7529ff0
    refactor(bbs): split session.go by responsibility, thread-safe writes
    
    session.go had grown to ~1,200 lines mixing terminal I/O, the main menu,
    weather and settings. It is now:
    
    - session.go: connection lifecycle, identity, splash, main loop
    - session_io.go: input, output and screen primitives
    - menu_main.go, menu_weather.go, menu_settings.go: the menus
    
    Functions moved verbatim. The one change in behaviour: writes to the SSH
    channel and the session error they record are now guarded by a mutex
    (wmu), with writeLocked for compound updates. This is groundwork for the
    chat, where incoming messages are drawn from another goroutine while the
    user types. Existing tests pass unchanged.
    

    Pablo Murad

  76. 5ed49b8
    feat: NDJSON dispatcher, SFTP area, async jobs
    
    SSH: one port now answers three ways. An interactive terminal gets the
    menu; a command (`ssh host hash input=hi`) or a pipe with no terminal gets
    a newline-delimited JSON dispatcher over a closed vocabulary with declared,
    validated arguments; the sftp subsystem gets a private per-key file area
    that tools read with file=NAME. Slow tools (net, links, data, path) can run
    as background jobs (async=true, then `job id=...` from any connection with
    the same key). Jobs live in memory.
    
    Anonymous sessions are now throwaway identities: defaults for the
    connection, nothing saved, no file area. Keying them by client IP made
    everyone behind one NAT share and overwrite each other's settings.
    
    HTTP: X-Forwarded-For / X-Real-IP are believed only from trusted proxies
    (server.trustedProxies, loopback by default), read right to left. Weather
    requests are rate limited per client IP (429 + Retry-After), dispatcher
    calls per key, and geocoder calls per UTC day (geo.dailyLookupLimit) to
    protect the OpenCage quota.
    
    OCR is removed everywhere: ocrpdf package, menu entry, config keys,
    translations and the ocrmypdf/tesseract packages in the Dockerfile. Old
    configs carrying ocr* keys will fail at startup; use the ones in deploy/.
    
    The storage purge is generalised to the file area (sweepStorageLoop).
    Tests added for the HTTP server, client IP resolution, rate limiting,
    dispatcher, jobs, file area and the SSH modes end to end. README and the
    CloudPanel runbook describe the current ports, modes and limits, and
    `make go-test-zig` runs the cgo tests without a C compiler.
    

    Pablo Murad

  77. 82964b8
    fix(renderer): v2 crashes and mis-drawing, plus test suite
    
    The v2 panel crashed on ordinary inputs and drew several things wrong.
    
    Crashes fixed:
    - location names over ~52 characters (negative strings.Repeat); the
      caption is now cut with an ellipsis and measured in terminal cells
    - interpolate: infinite recursion on 2 points, NaN spline on width 1
    - temperature diagrams with a single data point (division by zero)
    - rain sparkline narrower than its label, or series shorter than width
    - invalid timezone in drawDate, nil Options/Location in Render
    - negative wind bearings and fewer speeds than directions
    
    Drawing fixed:
    - daylight timeline was never drawn and started at UTC midnight; it now
      follows the sun's elevation from local midnight (covers polar day/night)
    - frame padding counted bytes, so rows with colour or accents were ragged
    - caption flags: quiet drops the prefix, no-city drops the city
    - spline overshoot showed temperatures never forecast and negative rain;
      interpolation is now clamped to the input range
    - emoji and wind rows are resampled to the 24 panel slots
    
    oneline: the plain emoji profile declared up-to-3-character symbols as
    one cell wide, and GetEmojiProfile dereferenced a nil options pointer.
    
    Adds a v2 test suite (99% coverage) with a regression test per fix.
    

    Pablo Murad

  78. a3f8029
    Remove GitHub URL from splash and about screens
    
    The project repository link no longer appears on the interactive front-end.
    Drops the attributionURL line from the splash and the about screen, and the
    now-unused constant. Author attribution and the pablomurad.com invite footer
    stay.
    

    Pablo Murad

  79. 0141e77
    feat(bbs): anonymous access, IP prefs, storage quota
    

    Pablo Murad

  80. b5a7c1f
    fix(deploy): docker prod compose, config and runbook
    

    Pablo Murad

  81. 341444d
    chore(deploy): add deployment configs and CloudPanel runbook
    
    - deploy/config.yaml: production config (BBS SSH + HTTP weather, ${VAR} secrets)
    - deploy/hammer.service: systemd unit (dedicated user, CAP_NET_BIND_SERVICE for :22)
    - deploy/nginx-hammer.conf: reverse proxy for the HTTP weather service
    - deploy/DEPLOY-CLOUDPANEL.md: step-by-step runbook for the CloudPanel VPS
      (web via reverse proxy -> 8080, SSH BBS direct on 2323)
    - .env.example: document GEOLITE2_LICENSE_KEY, symbolic placeholders
    

    Pablo Murad

  82. e919fa7
    docs: remove doc/ folder
    

    Pablo Murad

  83. c7f4b20
    feat: SSH tools platform, i18n, and build/port/format fixes
    
    - BBS front-end: interactive menu, per-key profiles, tool dispatcher
    - Native tools: net, qr, hash, jq, age, csv, b64, ids (+ optional links, data, path, ocr)
    - i18n for the BBS menu across all bundled languages
    - Move BBS SSH port 2222 -> 2323 (2222 already in use; 22 disallowed)
    - gofmt: reformat cache, ip, location, logging and v2 renderer files
    - Re-enable the gofmt linter in .golangci.yaml so formatting is enforced
    - build.sh: fail the build when no fonts are embedded, and warn on partial
      font sets, to avoid silently shipping a binary with degraded PNG output
    

    Pablo Murad

  84. fbb0774
    Add .env support and fix startup panics for minimal configs
    

    Pablo Murad

  85. da5b12c
    Add emoji weather card and redesign the BBS UI
    

    Pablo Murad

  86. 5b33d57
    Initial commit: Hammer
    

    Pablo Murad