version from 2026 :)

i started a plan to create a few key apps for this.

  • a live chat for end users, so, for each app, they can connect to get real time one on one help how to report bug or how to use it or how to get started as developer or tester
  • a packaged app that puts minimized apps to sleep (was linked elsewhere in this wiki)
  • an app to point users to source for a package and how to build it and how to submit patches (something gui and interfaces window manager and package manager and git etc)
  • an app to report bugs as screenshots or screencasts

currently work is in progress for item 1. a source code and schedule will be posted for it when it is ready. planned ideas for it

  • expected workflow
    • a web page has schedule for open hour a week
    • when open, user joins a chat
    • an irc bot auto-invites user to subchannel
  • requires a few admin things
    • webchat that autojoins on invite
    • irc bot that routes users to subchannels

Release 2026_04_06

This includes a basic prototype for live chat support. It includes a webchat that auto-joins on invite and shows the chat schedule, and a irc bot that invites the user to a channel where one on one help will be available. This software can be configured to run live chat support sessions for open source software packages, linux distributions, content and media sites, and so on. In the future it will be programmed to prompt the user for a summary of their inquiry, and to make it easier for volunteer helpers to follow which chats they are actively helping in.

Code Part 1: Edits to the web chat

The web chat https://codeberg.org/emersion/gamja was modified to auto-join users on invite for the user routing to work, and to show chat schedule at time of connection. The chat schedule should be shown more clearly with a countdown but I couldn't get this to work. If you can, please contact me and send me a diff.


diff --git a/components/app.js b/components/app.js
index cd4e04f..fc57193 100644
--- a/components/app.js
+++ b/components/app.js
@@ -173,6 +173,8 @@ export default class App extends Component {
            saslExternal: false,
            autoconnect: false,
            autojoin: [],
+           chat_time: null,
+           chat_days: [],
            ping: 0,
        },
        connectForm: true,
@@ -273,6 +275,12 @@ export default class App extends Component {
        if (typeof config.server.autoconnect === "boolean") {
            connectParams.autoconnect = config.server.autoconnect;
        }
+       if (typeof config.server.chat_time === "string") {
+           connectParams.chat_time = config.server.chat_time;
+       }
+       if (Array.isArray(config.server.chat_days)) {
+           connectParams.chat_days = config.server.chat_days;
+       }
        if (config.server.auth === "external") {
            connectParams.saslExternal = true;
        }
@@ -723,21 +731,21 @@ export default class App extends Component {
            }
        }
        if (msg.command === "INVITE" && client.isMyNick(msg.params[0])) {
-           msgUnread = Unread.HIGHLIGHT;
-
+//         msgUnread = Unread.HIGHLIGHT;
+//
            let channel = msg.params[1];
-           let notif = new Notification("Invitation to " + channel, {
-               body: msg.prefix.name + " has invited you to " + channel,
-               requireInteraction: true,
-               tag: "invite,server=" + serverID + ",from=" + msg.prefix.name + ",channel=" + channel,
-               actions: [{
-                   action: "accept",
-                   title: "Accept",
-               }],
-           });
-           if (notif) {
-               notif.addEventListener("click", (event) => {
-                   if (event.action === "accept") {
+//         let notif = new Notification("Invitation to " + channel, {
+//             body: msg.prefix.name + " has invited you to " + channel,
+//             requireInteraction: true,
+//             tag: "invite,server=" + serverID + ",from=" + msg.prefix.name + ",channel=" + channel,
+//             actions: [{
+//                 action: "accept",
+//                 title: "Accept",
+//             }],
+//         });
+//         if (notif) {
+//             notif.addEventListener("click", (event) => {
+//                 if (event.action === "accept") {
                        let stored = {
                            name: bufName,
                            server: client.params,
@@ -747,12 +755,12 @@ export default class App extends Component {
                            this.sendReadReceipt(client, stored);
                        }
                        this.open(channel, serverID);
-                   } else {
-                       // TODO: scroll to message
-                       this.switchBuffer({ server: serverID, name: bufName });
-                   }
-               });
-           }
+//                 } else {
+//                     // TODO: scroll to message
+//                     this.switchBuffer({ server: serverID, name: bufName });
+//                 }
+//             });
+//         }
        }
 
        // Open a new buffer if the message doesn't come from me or is a
diff --git a/components/connect-form.js b/components/connect-form.js
index ae8a59e..293fd02 100644
--- a/components/connect-form.js
+++ b/components/connect-form.js
@@ -28,6 +28,8 @@ export default class ConnectForm extends Component {
                rememberMe: props.params.autoconnect || false,
                username: props.params.username || "",
                realname: props.params.realname || "",
+               chat_days: props.params.chat_days || "",
+               chat_time: props.params.chat_time || "",
            };
        }
    }
@@ -54,6 +56,8 @@ export default class ConnectForm extends Component {
            realname: this.state.realname,
            saslPlain: null,
            autojoin: [],
+           chat_days: this.state.chat_days,
+           chat_time: this.state.chat_time,
        };
 
        if (this.state.password) {
@@ -111,6 +115,18 @@ export default class ConnectForm extends Component {
            `;
        }
 
+
+let chat_status = "Unknown"; let schedule = "TBC";
+if (this.props.params.chat_time && this.props.params.chat_days) {
+const now = new Date(), [h, m] = this.props.params.chat_time.split(':').map(Number);
+const days = this.props.params.chat_days.map(d => ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"].indexOf(d.slice(0,3)));
+let start = days.map(d => {
+    let s = new Date(now); s.setDate(now.getDate() + (d + 7 - now.getDay()) % 7);
+    s.setHours(h, m, 0, 0); if (now > s.getTime() + 36e5) s.setDate(s.getDate() + 7); return s;
+}).sort((a, b) => a - b)[0];
+const end = new Date(start.getTime() + 36e5);
+chat_status = `Now: ${now.toLocaleString()} | Start: ${start.toLocaleString()} | End: ${end.toLocaleString()}`;
+}
        let auth = null;
        if (this.props.auth !== "disabled" && this.props.auth !== "external" && this.props.auth !== "oauth2") {
            auth = html`
@@ -148,7 +164,7 @@ export default class ConnectForm extends Component {
 
        return html`
            
- Connect to IRC + STATUS: ${chat_status}

Code Part 2: IRC bot for routing

The chat bot does not actually chat, it only does these things:

  1. join a main channel
  2. join around 20 channels, numbered consecutively, where users will be provided help. (Helpers should join all of those.)
  3. mark all these channels as free
  4. auto-invite new users who joined 'main' channel, to these help-provider channels and mark them as busy
  5. when typed '!done' in a channel, it becomes marked as free

In future the bot will be improved to provide configuration settings for irc server, nick, and channels to join.


import time,threading
from irc import client

SERVER='irc.libera.chat'; PORT=6667; NICK='DebuxBot'
MAIN='#debux-test'
CHANS=[f'#debux-test-{i:03d}' for i in range(1,21)]

reactor=client.Reactor()
conn=reactor.server().connect(SERVER, PORT, NICK)

topic_state={ch: None for ch in CHANS}

def safe_send(func, *args):
    try:
        func(*args)
    except Exception:
        pass
    time.sleep(0.8)

def on_welcome(c, e):
    safe_send(c.join, MAIN)
    for ch in CHANS:
        safe_send(c.join, ch)
        topic_state[ch]='free'
        safe_send(c.topic, ch, 'free')

def on_join(c, e):
    if e.target != MAIN or e.source.nick == NICK:
        return
    target=None
    for ch in CHANS:
        if topic_state.get(ch)=='free':
            target=ch; break
    if target:
        safe_send(c.invite, e.source.nick, target)
        topic_state[target]='busy'
        safe_send(c.topic, target, 'busy')
    else:
        safe_send(c.kick, MAIN, e.source.nick, 'support is full, sorry, try again later')

def on_topic(c, e):
    if e.target in topic_state:
        topic_state[e.target]=e.arguments[0] if e.arguments else None

def on_pubmsg(c, e):
    nick = e.source.nick
    ch = e.target
    msg = e.arguments[0].strip() if e.arguments else ''
    if msg == '!done' and ch in topic_state and nick != NICK:
        topic_state[ch]='free'
        safe_send(c.topic, ch, 'free')

conn.add_global_handler('welcome', on_welcome)
conn.add_global_handler('join', on_join)
conn.add_global_handler('topic', on_topic)
conn.add_global_handler('pubmsg', on_pubmsg)

def loop():
    reactor.process_forever()

t=threading.Thread(target=loop, daemon=True)
t.start()
try:
    while t.is_alive():
        time.sleep(1)
except KeyboardInterrupt:
    conn.quit('bye')

Demo

Demo is available at http://chris-amadeus.bnr.la as of 2026.04.06 and this demo may stop working or be disabled at any time.

version from 2023 :)

minimal:

  • video bug reports
    • pre-install obs-studio
    • add obs-studio replay buffer to autostart by default
    • write debux-visual-bug-report script that allows to send a message (to where?) with included video, option to trim/clip or select a time stamp
  • chat
    • include an IRC client auto-connecting to #debux as desktop shortcut and where else appropriate
  • distro
    • choose a DE
    • write debian blend or archlinux iso

desired:

  • package the xappnap app (external)
  • some means to browse source of each app, when installing an app