diff --git a/gitlog.jsonl b/gitlog.jsonl
index 764c7df..010d184 100644
--- a/gitlog.jsonl
+++ b/gitlog.jsonl
@@ -990,3 +990,500 @@
 {"repo": ".", "date": "2025-05-17", "line": "feat: Refactor index.html with improved styling and features overview", "commit": "c0b4ba715c329273e4f5684d1ec2e231e5a1c7e7", "diff": "commit c0b4ba715c329273e4f5684d1ec2e231e5a1c7e7\nAuthor: retoor <retoor@molodetz.nl>\nDate:   Sat May 17 00:53:27 2025 +0200\n\n    t:\n\ndiff --git a/src/snek/templates/index.html b/src/snek/templates/index.html\nindex a1e8894..e12fe2b 100644\n--- a/src/snek/templates/index.html\n+++ b/src/snek/templates/index.html\n@@ -1,31 +1,288 @@\n-<!DOCTYPE html>\n+\n+                        <!DOCTYPE html>\n+                        <html lang=\"en\">\n+                        <head>\n+                            <meta charset=\"UTF-8\">\n+                            <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n+\t\t\t\t\t\t\t<style>\n+\t\t\t\t\t\t\t\tbody {\n+\t\t\t\t\t\t\t\t}\n+\n+\t\t\t\t\t\t\t\t\n+    * { margin:0; padding:0; box-sizing:border-box; }\n+    body {\n+      font-family: 'Segoe UI',sans-serif;\n+      line-height:1.5;\n+    }\n+    a:hover { text-decoration: underline; }\n+\n+    .container { width: 90%; max-width: 960px; margin: auto; padding: 2rem 0; }\n+\n+    .hero {\n+      text-align: center;\n+      padding: 4rem 0;\n+    }\n+    .hero h1 {\n+      font-size: 3rem;\n+      -webkit-background-clip: text;\n+      color: transparent;\n+    }\n+    .hero p {\n+      font-size: 1.2rem;\n+      margin: 1rem 0 2rem;\n+    }\n+    .btn {\n+      display: inline-block;\n+      padding: .75rem 1.5rem;\n+      margin: .5rem;\n+      font-weight: bold;\n+      border-radius: 4px;\n+      transition: background .2s;\n+    }\n+\n+    .grid {\n+      display: grid;\n+      grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));\n+      gap: 1.5rem;\n+      margin-top: 2rem;\n+    }\n+    .card {\n+      border-radius: 6px;\n+      padding: 1.5rem;\n+      box-shadow: 0 2px 6px rgba(0,0,0,0.6);\n+    }\n+    .card h3 {\n+      margin-bottom: .75rem;\n+    }\n+    .card ul {\n+      list-style: disc inside;\n+      margin-top: .5rem;\n+    }\n+\n+    footer {\n+      text-align: center;\n+      font-size: .9rem;\n+      padding: 2rem 0;\n+    }\n+    footer code {\n+      padding: 2px 4px;\n+      border-radius: 3px;\n+    }\n+\n+    @media (max-width: 480px) {\n+      .hero h1 { font-size: 2.4rem; }\n+      .btn { width: 100%; box-sizing: border-box; text-align:center; }\n+    }\n+  \n+\n+\t\t\t\t\t\t\t</style>\n+                        </head>\n+                        <body>\n+                            <!DOCTYPE html>\n <html lang=\"en\">\n <head>\n-  <meta charset=\"UTF-8\">\n-  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n-  <title>Snek chat by Molodetz</title>\n-    <link rel=\"stylesheet\" href=\"generic-form.css\">\n-    <link rel=\"stylesheet\" href=\"base.css\">\n-<style>\n-  .registration-container {\n-        max-width: 300px;\n-        margin: 20px auto;\n-        padding: 20px;\n-  }\n-</style>\n-    <script src=\"/fancy-button.js\"></script>\n+  <meta charset=\"UTF-8\" />\n+  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n+  <title>Snek \u2013 The Ultimate Web Community</title>\n+  <style>\n+    * { margin:0; padding:0; box-sizing:border-box; }\n+    body {\n+      font-family: 'Segoe UI',sans-serif;\n+      line-height:1.5;\n+    }\n+    a:hover { text-decoration: underline; }\n+\n+    .container { width: 90%; max-width: 960px; margin: auto; padding: 2rem 0; }\n+\n+    .hero {\n+      text-align: center;\n+      padding: 4rem 0;\n+    }\n+    .hero h1 {\n+      font-size: 3rem;\n+      -webkit-background-clip: text;\n+      color: transparent;\n+    }\n+    .hero p {\n+      font-size: 1.2rem;\n+      margin: 1rem 0 2rem;\n+    }\n+    .btn {\n+      display: inline-block;\n+      padding: .75rem 1.5rem;\n+      margin: .5rem;\n+      font-weight: bold;\n+      border-radius: 4px;\n+      transition: background .2s;\n+    }\n+\n+    .grid {\n+      display: grid;\n+      grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));\n+      gap: 1.5rem;\n+      margin-top: 2rem;\n+    }\n+    .card {\n+      border-radius: 6px;\n+      padding: 1.5rem;\n+      box-shadow: 0 2px 6px rgba(0,0,0,0.6);\n+    }\n+    .card h3 {\n+      margin-bottom: .75rem;\n+    }\n+    .card ul {\n+      list-style: disc inside;\n+      margin-top: .5rem;\n+    }\n+\n+    footer {\n+      text-align: center;\n+      font-size: .9rem;\n+      padding: 2rem 0;\n+    }\n+    footer code {\n+      padding: 2px 4px;\n+      border-radius: 3px;\n+    }\n+\n+    @media (max-width: 480px) {\n+      .hero h1 { font-size: 2.4rem; }\n+      .btn { width: 100%; box-sizing: border-box; text-align:center; }\n+    }\n+  </style>\n </head>\n <body>\n-  <div class=\"registration-container\">\n+\n+  <header class=\"container hero\">\n     <h1>Snek</h1>\n-    <p style=\"padding-bottom:20px\">Rocket Chat got bloated, too commercialized,\n-    So Snek came through, lean and optimized.</p>\n-    <div style=\"text-align: center;\">\n-    <fancy-button url=\"/login.html\" text=\"Login\"></fancy-button>\n-    <span style=\"padding:10px;\">OR</span>\n-    <fancy-button url=\"/register.html\" text=\"Register\"></fancy-button>\n-    </div>\n-  </div>\n+    <p>The Ultimate Web Community for Devs, Testers &amp; AI Enthusiasts</p>\n+    <a href=\"/login.html\" class=\"btn\">Login</a>\n+    <a href=\"/register.html\" class=\"btn\">Register</a>\n+  </header>\n+\n+  <main class=\"container\">\n+\n+    <section id=\"features\" class=\"grid\">\n+      <div class=\"card\">\n+        <h3>File Sharing</h3>\n+        <ul>\n+          <li>SFTP storage with your Snek credentials</li>\n+          <li>WebDAV support \u2013 same login</li>\n+        </ul>\n+      </div>\n+\n+      <div class=\"card\">\n+        <h3>Git Repositories</h3>\n+        <ul>\n+          <li>Configure repos for any official Git client</li>\n+          <li>Instant setup, push &amp; pull</li>\n+        </ul>\n+      </div>\n+\n+      <div class=\"card\">\n+        <h3>AI Powerhouse</h3>\n+        <ul>\n+          <li>Chat with free &amp; commercial AIs</li>\n+          <li>Generate AI-powered images</li>\n+          <li>Build your own AI bots in &lt;5 minutes (copy/paste example)</li>\n+        </ul>\n+      </div>\n+\n+      <div class=\"card\">\n+        <h3>Dev &amp; Terminal</h3>\n+        <ul>\n+          <li>Ubuntu web terminal in-browser</li>\n+          <li>Full profile &amp; permissions management</li>\n+        </ul>\n+      </div>\n+\n+      <div class=\"card\">\n+        <h3>Chat &amp; Media</h3>\n+        <ul>\n+          <li>Upload any file type in chat</li>\n+          <li>Rich media support (audio, video, images\u2026)</li>\n+          <li>Direct messaging with other users</li>\n+        </ul>\n+      </div>\n+\n+      <div class=\"card\">\n+        <h3>Privacy &amp; Community</h3>\n+        <ul>\n+          <li>No logging\u2014never even your IP</li>\n+          <li>No email required to sign up</li>\n+          <li>Multi-national, open community</li>\n+          <li>Hacking encouraged!</li>\n+        </ul>\n+      </div>\n+\n+      <div class=\"card\">\n+        <h3>Customization &amp; Deployment</h3>\n+        <ul>\n+          <li>Full layout &amp; theme customization</li>\n+          <li>Install as a PWA on your phone</li>\n+          <li>Optionally self-host: <code>pip install snek</code>, zero config</li>\n+        </ul>\n+      </div>\n+    </section>\n+\n+    <section id=\"signup\" style=\"text-align:center; margin:4rem 0;\">\n+      <h2>Ready to join?</h2>\n+      <p>No email. No logs. Just sign up, pick a username, and dive in!</p>\n+      <a href=\"/register\" class=\"btn\">Sign Up Now</a>\n+    </section>\n+\n+    <section id=\"selfhost\" style=\"text-align:center; margin-bottom:4rem;\">\n+      <h2>Self-Host in Seconds</h2>\n+      <p>Just run:</p>\n+snek serve\n+      </pre>\n+      <p>No configuration required\u2014it's that simple.</p>\n+    </section>\n+\n+  </main>\n+\n+  <footer>\n+    <p>&copy; 2025 Snek \u2013 Join our global community of developers, testers &amp; AI enthusiasts.</p>\n+  </footer>\n+\n </body>\n </html>"}
 {"repo": ".", "date": "2025-05-17", "line": "feat: Refactor chat input component with improved auto-completion and live typing functionality", "commit": "48c3daf3983e3b6e04a0c5888febceb69db9d661", "diff": "commit 48c3daf3983e3b6e04a0c5888febceb69db9d661\nAuthor: retoor <retoor@molodetz.nl>\nDate:   Sat May 17 00:54:15 2025 +0200\n\n    Update.\n\ndiff --git a/src/snek/static/chat-input.js b/src/snek/static/chat-input.js\nindex c1d767d..2d0914e 100644\n--- a/src/snek/static/chat-input.js\n+++ b/src/snek/static/chat-input.js\n@@ -1,69 +1,234 @@\n-\n-\n-\n-\n-class ChatInputElement extends HTMLElement {\n-  _chatWindow = null \n-  constructor() {\n-    super();\n-    this.attachShadow({ mode: 'open' });\n-    this.component = document.createElement('div');\n-    this.shadowRoot.appendChild(this.component);\n-  }\n-  set chatWindow(value){\n-    this._chatWindow = value \n-\n-  }\n-  get chatWindow(){\n-    return this._chatWindow \n-  }\n-  get channelUid() {\n-    return this.chatWindow.channel.uid\n-  }\n-  connectedCallback() {\n-    const link = document.createElement('link');\n-    link.rel = 'stylesheet';\n-    link.href = '/base.css';\n-    this.component.appendChild(link);\n-\n-    this.container = document.createElement('div');\n-    this.container.classList.add('chat-input');\n-    this.container.innerHTML = `\n-      <textarea placeholder=\"Type a message...\" rows=\"2\"></textarea>\n-      <upload-button></upload-button>\n-    `;\n-    this.textBox = this.container.querySelector('textarea');\n-    this.uploadButton = this.container.querySelector('upload-button');\n-    this.uploadButton.chatInput = this \n-    this.textBox.addEventListener('input', (e) => {\n-      this.dispatchEvent(new CustomEvent('input', { detail: e.target.value, bubbles: true }));\n-      const message = e.target.value;\n-      const button = this.container.querySelector('button');\n-      button.disabled = !message;\n-    });\n-\n-    this.textBox.addEventListener('change', (e) => {\n-      e.preventDefault();\n-      this.dispatchEvent(new CustomEvent('change', { detail: e.target.value, bubbles: true }));\n-      console.error(e.target.value);\n-    });\n-\n-    this.textBox.addEventListener('keydown', (e) => {\n-      if (e.key === 'Enter' && !e.shiftKey) {\n-        e.preventDefault();\n-        const message = e.target.value.trim();\n-        if (!message) return;\n-        this.dispatchEvent(new CustomEvent('submit', { detail: message, bubbles: true }));\n-        e.target.value = '';\n-      }\n-    });\n-\n-    this.component.appendChild(this.container);\n-  }\n+\n+import { app } from '../app.js';\n+\n+class ChatInputComponent extends HTMLElement {\n+    autoCompletions = {\n+        'example 1': () => {\n+\n+        },\n+        'example 2': () => {\n+\n+        }\n+    }\n+\n+    constructor() {\n+        super();\n+        this.lastUpdateEvent = new Date();\n+        this.textarea = document.createElement(\"textarea\");\n+        this._value = \"\";\n+        this.value = this.getAttribute(\"value\") || \"\";\n+        this.previousValue = this.value;\n+        this.lastChange = new Date();\n+        this.changed = false;\n+    }\n+\n+    get value() {\n+        return this._value;\n+    }\n+\n+    set value(value) {\n+        this._value = value || \"\";\n+        this.textarea.value = this._value;\n+    }\n+\n+    resolveAutoComplete() {\n+        let count = 0;\n+        let value = null;\n+        Object.keys(this.autoCompletions).forEach((key) => {\n+            if (key.startsWith(this.value)) {\n+                count++;\n+                value = key;\n+            }\n+        });\n+        if (count == 1)\n+            return value;\n+        return null;\n+    }\n+\n+    isActive() {\n+        return document.activeElement === this.textarea;\n+    }\n+\n+    focus() {\n+        this.textarea.focus();\n+    }\n+\n+    connectedCallback() {\n+        this.liveType = this.getAttribute(\"live-type\") === \"true\";\n+        this.liveTypeInterval = parseInt(this.getAttribute(\"live-type-interval\")) || 3;\n+        this.channelUid = this.getAttribute(\"channel\");\n+        this.messageUid = null;\n+\n+        this.classList.add(\"chat-input\");\n+\n+        this.textarea.setAttribute(\"placeholder\", \"Type a message...\");\n+        this.textarea.setAttribute(\"rows\", \"2\");\n+\n+        this.appendChild(this.textarea);\n+\n+        this.uploadButton = document.createElement(\"upload-button\");\n+        this.uploadButton.setAttribute(\"channel\", this.channelUid);\n+        this.uploadButton.addEventListener(\"upload\", (e) => {\n+            this.dispatchEvent(new CustomEvent(\"upload\", e));\n+        });\n+        this.uploadButton.addEventListener(\"uploaded\", (e) => {\n+            this.dispatchEvent(new CustomEvent(\"uploaded\", e));\n+        });\n+\n+        this.appendChild(this.uploadButton);\n+\n+        this.textarea.addEventListener(\"keyup\", (e) => {\n+            if(e.key === 'Enter' && !e.shiftKey) {\n+                this.value = ''\n+                e.target.value = '';\n+                return \n+            }\n+            this.value = e.target.value;\n+            this.changed = true;\n+            this.update();\n+        });\n+\n+        this.textarea.addEventListener(\"keydown\", (e) => {\n+            this.value = e.target.value;\n+            if (e.key === \"Tab\") {\n+                e.preventDefault();\n+                let autoCompletion = this.resolveAutoComplete();\n+                if (autoCompletion) {\n+                    e.target.value = autoCompletion;\n+                    this.value = autoCompletion;\n+                    return;\n+                }\n+            }\n+            if (e.key === 'Enter' && !e.shiftKey) {\n+                e.preventDefault();\n+\n+                const message = e.target.value;\n+                this.messageUid = null;\n+                this.value = '';\n+                this.previousValue = '';\n+\n+                if (!message) {\n+                    return;\n+                }\n+\n+                let autoCompletion = this.autoCompletions[message];\n+                if (autoCompletion) {\n+                    this.value = '';\n+                    this.previousValue = '';\n+                    e.target.value = '';\n+                    autoCompletion();\n+                    return;\n+                }\n+\n+                e.target.value = '';\n+                this.value = '';\n+                this.messageUid = null;\n+                this.sendMessage(this.channelUid, message).then((uid) => {\n+                    this.messageUid = uid;\n+                });\n+            }\n+        });\n+\n+        this.changeInterval = setInterval(() => {\n+            if (!this.liveType) {\n+                return;\n+            }\n+            if (this.value !== this.previousValue) {\n+                if (this.trackSecondsBetweenEvents(this.lastChange, new Date()) >= this.liveTypeInterval) {\n+                    this.value = '';\n+                    this.previousValue = '';\n+                }\n+                this.lastChange = new Date();\n+            }\n+            this.update();\n+        }, 300);\n+\n+        this.addEventListener(\"upload\", (e) => {\n+            this.focus();\n+        });\n+        this.addEventListener(\"uploaded\", function (e) {\n+            let message = \"\";\n+            e.detail.files.forEach((file) => {\n+                message += `[${file.name}](/channel/attachment/${file.relative_url})`;\n+            });\n+            app.rpc.sendMessage(this.channelUid, message);\n+        });\n+    }\n+\n+    trackSecondsBetweenEvents(event1Time, event2Time) {\n+        const millisecondsDifference = event2Time.getTime() - event1Time.getTime();\n+        return millisecondsDifference / 1000;\n+    }\n+\n+    newMessage() {\n+        if (!this.messageUid) {\n+            this.messageUid = '?';\n+        }\n+\n+        this.sendMessage(this.channelUid, this.value).then((uid) => {\n+            this.messageUid = uid;\n+        });\n+    }\n+\n+    updateMessage() {\n+        if (this.value[0] == \"/\") {\n+            return false;\n+        }\n+        if (!this.messageUid) {\n+            this.newMessage();\n+            return false;\n+        }\n+        if (this.messageUid === '?') {\n+            return false;\n+        }\n+        if (typeof app !== \"undefined\" && app.rpc && typeof app.rpc.updateMessageText === \"function\") {\n+            app.rpc.updateMessageText(this.messageUid, this.value);\n+        }\n+    }\n+\n+    updateStatus() {\n+        if (this.liveType) {\n+            return;\n+        }\n+        if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) > 1) {\n+            this.lastUpdateEvent = new Date();\n+            if (typeof app !== \"undefined\" && app.rpc && typeof app.rpc.set_typing === \"function\") {\n+                app.rpc.set_typing(this.channelUid);\n+            }\n+        }\n+    }\n+\n+    update() {\n+        const expired = this.trackSecondsBetweenEvents(this.lastChange, new Date()) >= this.liveTypeInterval;\n+        const changed = (this.value !== this.previousValue);\n+\n+        if (changed || expired) {\n+            this.lastChange = new Date();\n+            this.updateStatus();\n+        }\n+\n+        this.previousValue = this.value;\n+\n+        if (this.liveType && expired) {\n+            this.value = \"\";\n+            this.previousValue = \"\";\n+            this.messageUid = null;\n+            return;\n+        }\n+\n+        if (changed) {\n+            if (this.liveType) {\n+                this.updateMessage();\n+            }\n+        }\n+    }\n+\n+    async sendMessage(channelUid, value) {\n+        if (!value.trim()) {\n+            return null;\n+        }\n+        return await app.rpc.sendMessage(channelUid, value);\n+    }\n }\n \n-customElements.define('chat-input', ChatInputElement);\n+customElements.define('chat-input', ChatInputComponent);\ndiff --git a/src/snek/templates/app.html b/src/snek/templates/app.html\nindex 7baa67a..596b5d1 100644\n--- a/src/snek/templates/app.html\n+++ b/src/snek/templates/app.html\n@@ -18,6 +18,7 @@\n   <script src=\"/file-manager.js\" type=\"module\"></script>\n   <script src=\"/user-list.js\"></script>\n   <script src=\"/message-list.js\" type=\"module\"></script>\n+  <script src=\"/chat-input.js\" type=\"module\"></script>\n   <link rel=\"stylesheet\" href=\"/user-list.css\">\n \n   <link rel=\"stylesheet\" href=\"/base.css\">\ndiff --git a/src/snek/templates/web.html b/src/snek/templates/web.html\nindex 02e60df..94d0ac5 100644\n--- a/src/snek/templates/web.html\n+++ b/src/snek/templates/web.html\n@@ -4,10 +4,6 @@\n \n {% block main %}\n \n-\n-\n-\n-\n <section class=\"chat-area\">\n     <message-list class=\"chat-messages\">\n     {% for message in messages %}\n@@ -16,10 +12,7 @@\n         {% endautoescape %}\n     {% endfor %}\n     </message-list>\n-    <div class=\"chat-input\">\n-        <textarea list=\"chat-input-autocomplete-items\" placeholder=\"Type a message...\" rows=\"2\" autocomplete=\"on\"></textarea>\n-        <upload-button channel=\"{{ channel.uid.value }}\"></upload-button>\n-    </div>\n+    <chat-input live-type=\"false\" channel=\"{{ channel.uid.value }}\"></chat-input>\n </section>\n {% include \"dialog_help.html\" %}\n {% include \"dialog_online.html\" %}\n@@ -27,11 +20,8 @@\n     import { app } from \"/app.js\";\n     import { Schedule } from \"/schedule.js\";\n     const channelUid = \"{{ channel.uid.value }}\";\n-\n-    function getInputField(){\n-        return document.querySelector(\"textarea\")\n-    }\n-    getInputField().autoComplete = {\n+    const chatInputField = document.querySelector(\"chat-input\");\n+    chatInputField.autoCompletions = {\n         \"/online\": () =>{\n             showOnline();\n         },\n@@ -39,117 +29,15 @@\n             document.querySelector(\".chat-messages\").innerHTML = '';\n         },\n         \"/live\": () =>{\n-            getInputField().liveType = !getInputField().liveType\n+            \n+            chatInputField.liveType = !chatInputField.liveType\n         },\n         \"/help\": () => {\n             showHelp();\n         }\n-    }\n-\n-\n-    function initInputField(textBox) {\n-        if(textBox.liveType == undefined){\n-            textBox.liveType = false\n-        }\n-        let typeTimeout = null;\n-        textBox.addEventListener('keydown',async (e) => {\n-            if(typeTimeout){\n-                clearTimeout(typeTimeout)\n-                typeTimeout = null\n-            }\n-            if(e.target.liveType){\n-                typeTimeout = setTimeout(()=>{\n-                    e.target.lastMessageUid = null\n-                    e.target.value = ''\n-                },3000)\n-            }\n-            if(e.key === \"ArrowUp\"){\n-                const value = findDivAboveText(e.target.value).querySelector('.text')\n-                e.target.value = value.textContent\n-                console.info(\"HIERR\")\n-                return\n-            }\n-            if (e.key === \"Tab\") {\n-\n-                const message = e.target.value.trim();\n-                if (!message) {\n-                    return\n-                }\n-                let autoCompleteHandler = null;\n-                Object.keys(e.target.autoComplete).forEach((key)=>{\n-                    if(key.startsWith(message)){\n-                        if(autoCompleteHandler){\n-                            return \n-                        }\n-                        autoCompleteHandler = key\n-                    }\n-                })\n-                if(autoCompleteHandler){\n-                    e.preventDefault();\n-                    e.target.value = autoCompleteHandler;\n-                    return\n-                }\n-            }\n-            if (e.key === 'Enter' && !e.shiftKey) {\n-                e.preventDefault();\n-                const message = e.target.value.trim();\n-                if (!message) {\n-                    return\n-                }\n-                let autoCompleteHandler = e.target.autoComplete[message]\n-                if(autoCompleteHandler){\n-                    const value = message;\n-                    e.target.value = '';\n-                    autoCompleteHandler(value)\n-                    return\n-                }\n-\n-                e.target.value = '';\n-                if(textBox.liveType && textBox.lastMessageUid && textBox.lastMessageUid != '?'){\n-                        \n-                \n-                    app.rpc.updateMessageText(textBox.lastMessageUid, message)\n-                textBox.lastMessageUid = null\n-                    return \n-                }\n-\n-                const messageResponse = await app.rpc.sendMessage(channelUid, message);\n-                \n-\t    }else{\n-\t\tif(textBox.liveType){\n-            if(e.target.value.endsWith(\"\\n\") || e.target.value.endsWith(\" \")){\n-                return\n-            } \n-            if(e.target.value[0] == \"/\"){\n-                return\n-            }\n-            if(!textBox.lastMessageUid){\n-                textBox.lastMessageUid = '?'\n-                app.rpc.sendMessage(channelUid, e.target.value).then((messageResponse)=>{\n-                    textBox.lastMessageUid = messageResponse\n-                })\n-            }\n-            if(textBox.lastMessageUid == '?'){\n-                return;\n-            }\n-            app.rpc.updateMessageText(textBox.lastMessageUid, e.target.value)\n-        }else{\n-            app.rpc.set_typing(channelUid)\n-        }\n-\n-        \n-\t    }\n-        });\n-        document.querySelector(\"upload-button\").addEventListener(\"upload\",function(e){\n-            getInputField().focus();\n-        })\n-        document.querySelector(\"upload-button\").addEventListener(\"uploaded\",function(e){\n-            let message = \"\"\n-            e.detail.files.forEach((file)=>{\n-                message += `[${file.name}](/channel/attachment/${file.relative_url})`\n-            })\n-            app.rpc.sendMessage(channelUid,message)\n-        })\n+     }\n+    \n+        const textBox = document.querySelector(\"chat-input\").textarea\n         textBox.addEventListener(\"paste\", async (e) => {\n             try {\n                 const clipboardItems = await navigator.clipboard.read();\n@@ -168,7 +56,7 @@\n                 }\n \n                 if (dt.items.length > 0) {\n-                    const uploadButton = document.querySelector(\"upload-button\");\n+                    const uploadButton = chatInputField.uploadButton\n                     const input = uploadButton.shadowRoot.querySelector('.file-input')\n                     input.files = dt.files;\n \n@@ -187,7 +75,7 @@\n \n             const dt = e.dataTransfer;\n             if (dt.items.length > 0) {\n-                const uploadButton = document.querySelector(\"upload-button\");\n+                const uploadButton = chatInputField.uploadButton\n                 const input = uploadButton.shadowRoot.querySelector('.file-input')\n                 input.files = dt.files;\n \n@@ -197,13 +85,16 @@\n         chatInput.addEventListener(\"dragover\", async (e) => {\n             e.preventDefault();\n             e.dataTransfer.dropEffect = \"link\";\n+        \n+\n         })\n \n-        textBox.focus();\n-    }\n+            chatInputField.textarea.focus();\n+\n+    \n \n     function replyMessage(message) {\n-        const field = getInputField()\n+        const field = chatInputField\n         field.value = \"```markdown\\n> \" + (message || '') + \"\\n```\\n\";\n         field.focus();\n     }\n@@ -294,8 +185,8 @@\n         lastMessage = messagesContainer.querySelector(\".message:last-child\");\n         if (doScrollDown) {\n             lastMessage?.scrollIntoView({ block: \"end\", inline: \"nearest\" });\n-             const inputBox = document.querySelector(\".chat-input\");\n-             inputBox.scrollIntoView({ block: \"end\", inline: \"nearest\" });\n+             \n+             chatInputField.scrollIntoView({ block: \"end\", inline: \"nearest\" });\n         }\n     }\n \n@@ -378,17 +269,17 @@\n             messagesContainer.querySelector(\".message:last-child\").scrollIntoView({ block: \"end\", inline: \"nearest\" });\n                 setTimeout(() => {\n                     \n-                    getInputField().focus();\n+                    chatInputField.focus();\n                 },500)\n \n             }\n         }\n         if (event.shiftKey && event.key === 'G') {\n-            if(document.activeElement != getInputField()){\n+            if(chatInputField.isActive()){\n             \n                 updateLayout(true);\n                 setTimeout(() => {\n-                    getInputField().focus();\n+                    chatInputField.focus();\n                 },500)\n             }\n \n@@ -432,7 +323,6 @@\n         document.body.removeChild(overlay);\n       });\n     });\n-    initInputField(getInputField());\n     updateLayout(true);\n </script>\n {% endblock %}"}
 {"repo": ".", "date": "2025-05-17", "line": "feat: Added star animation to sandbox page", "commit": "e79abf4a26454cddf766cd1ba138554817c820cb", "diff": "commit e79abf4a26454cddf766cd1ba138554817c820cb\nAuthor: retoor <retoor@molodetz.nl>\nDate:   Sat May 17 17:46:59 2025 +0200\n\n    Update stars.\n\ndiff --git a/src/snek/static/sandbox.css b/src/snek/static/sandbox.css\nnew file mode 100644\nindex 0000000..1419fe4\n--- /dev/null\n+++ b/src/snek/static/sandbox.css\n@@ -0,0 +1,28 @@\n+    .star {\n+      position: absolute;\n+      width: 2px;\n+      height: 2px;\n+      border-radius: 50%;\n+      opacity: 0;\n+      animation: twinkle ease-in-out infinite;\n+    }\n+\n+    @keyframes twinkle {\n+      0%, 100% { opacity: 0; }\n+      50%      { opacity: 1; }\n+    }\n+\n+    .content {\n+      position: relative;\n+      z-index: 1;\n+      font-family: sans-serif;\n+      text-align: center;\n+      top: 40%;\n+      transform: translateY(-40%);\n+    }\n+\ndiff --git a/src/snek/templates/app.html b/src/snek/templates/app.html\nindex 596b5d1..ceef196 100644\n--- a/src/snek/templates/app.html\n+++ b/src/snek/templates/app.html\n@@ -19,6 +19,7 @@\n   <script src=\"/user-list.js\"></script>\n   <script src=\"/message-list.js\" type=\"module\"></script>\n   <script src=\"/chat-input.js\" type=\"module\"></script>\n+  <link rel=\"stylesheet\" href=\"/sandbox.css\">\n   <link rel=\"stylesheet\" href=\"/user-list.css\">\n \n   <link rel=\"stylesheet\" href=\"/base.css\">\n@@ -78,5 +79,6 @@ let installPrompt = null\n     \n         ;\n     </script>\n+    {% include \"sandbox.html\" %}\n </body>\n </html>\ndiff --git a/src/snek/templates/sandbox.html b/src/snek/templates/sandbox.html\nnew file mode 100644\nindex 0000000..6fd9b3f\n--- /dev/null\n+++ b/src/snek/templates/sandbox.html\n@@ -0,0 +1,31 @@\n+\n+\t\t\t\t\t\t\t<script>\n+                            \t\n+    const STAR_COUNT = 200;\n+    const body = document.body;\n+\n+    for (let i = 0; i < STAR_COUNT; i++) {\n+      const star = document.createElement('div');\n+      star.classList.add('star');\n+\n+      star.style.left  = Math.random() * 100 + '%';\n+      star.style.top   = Math.random() * 100 + '%';\n+\n+      star.style.width  = size + 'px';\n+      star.style.height = size + 'px';\n+\n+      star.style.animationDuration = duration + 's';\n+      star.style.animationDelay    = delay + 's';\n+\n+      body.appendChild(star);\n+    }\n+  \n+\n+\t\t\t\t\t\t\t</script>"}
+{"repo": ".", "date": "2025-01-17", "line": "feat: Initial project setup with basic structure and boilerplate files", "commit": "66f89429366042c77599f3a9b8c1a7aecf976a4f"}
+{"repo": ".", "date": "2025-01-17", "line": "feat: Initialized project with basic description and setup instructions", "commit": "46a27405aeb8ec426fd1c686a2c090f9fe9c0e62"}
+{"repo": ".", "date": "2025-01-18", "line": "feat: Added basic login and register pages with styling.", "commit": "a7446d131413da9f013a56d3541192d8ab1e22b0"}
+{"repo": ".", "date": "2025-01-18", "line": "docs(docker): Add restart policy to snek service", "commit": "2e3b85d7f739160783e7c5552f1306298047704a"}
+{"repo": ".", "date": "2025-01-24", "line": "feat: Refactor project structure and introduce generic form components.", "commit": "ba83922660dade77dcb96e8ba9c73cfcba8c2b81"}
+{"repo": ".", "date": "2025-01-24", "line": "feat: Complete system with basic functionality and initial views", "commit": "d20079f3ed8f261bcda0f5379f4c9e23ee941527"}
+{"repo": ".", "date": "2025-01-24", "line": "feat: Use \"/back\" URL for back button navigation", "commit": "4b48485bcc9f73662a1eb4ab3142bb0f4d5bef7a"}
+{"repo": ".", "date": "2025-01-24", "line": "feat: Implement size attribute for fancy-button", "commit": "0271e3f9719a4155fc5be37c36feca865932b0c1"}
+{"repo": ".", "date": "2025-01-24", "line": "fix: Updated button URLs in index.html", "commit": "bda93e354f4691483dbbef29949672ab0989b7e0"}
+{"repo": ".", "date": "2025-01-24", "line": "feat: Style adjustments and layout improvements for responsive design", "commit": "757b67b78c2f396df7ac7b5706f98833aedfb85b"}
+{"repo": ".", "date": "2025-01-24", "line": "feat: Refactor styling and layout for improved aesthetics and responsiveness", "commit": "6ba6121988dae019d4c4c0a3b8592b443f094065"}
+{"repo": ".", "date": "2025-01-24", "line": "feat: Refactor login and register form endpoints to use /login.json and /register.json", "commit": "c1eeacc0b415ef770418bb053d18ac0ffa4f64c2"}
+{"repo": ".", "date": "2025-01-24", "line": "feat: Implemented caching decorator for asynchronous functions", "commit": "21ab5628b072320ae0851819116e57539b2397d9"}
+{"repo": ".", "date": "2025-01-24", "line": "feat: Switch to gunicorn and add docs and about pages", "commit": "8486c22c325ba358bd48766c518f7c7bd30059eb"}
+{"repo": ".", "date": "2025-01-24", "line": "feat: Added API documentation with code examples and HTML template", "commit": "aecd9f844ef0a277a55aa536db3336362e8db353"}
+{"repo": ".", "date": "2025-01-24", "line": "docs: Added styling for dialog element", "commit": "be9489f939b3518f9c3a73b9e54ba0f9d34ae24c"}
+{"repo": ".", "date": "2025-01-24", "line": "feat: Implemented user registration with username availability check and email field.", "commit": "2ba55f692dfc1b60ac55d514b182fb8834cb99bb"}
+{"repo": ".", "date": "2025-01-24", "line": "feat: Add documentation subapp and markdown extension", "commit": "18b76ebd5e2f11451db04800d426a16b1ef1dd14"}
+{"repo": ".", "date": "2025-01-24", "line": "feat: Refactor form validation and rendering for improved consistency", "commit": "9b93403a93ac0b03a57fb5dc10db5c35349c4d6f"}
+{"repo": ".", "date": "2025-01-24", "line": "feat: Added documentation and basic API examples", "commit": "b56371994f5d3f5c1aa5d63c28efd18856ea8e9b"}
+{"repo": ".", "date": "2025-01-24", "line": "feat: Improve document handling and error responses", "commit": "dae877113c76b6f0eded7b2d63ef921123a2b559"}
+{"repo": ".", "date": "2025-01-25", "line": "feat: Added session support and login functionality", "commit": "5c69e14d7cfae8da4efab776165cc8e466edcc41"}
+{"repo": ".", "date": "2025-01-25", "line": "feat: Implemented status endpoint with user information", "commit": "352d2deb12a471bc90425961849fb2e92da3ab16"}
+{"repo": ".", "date": "2025-01-25", "line": "feat: Refactor session management and improve status endpoint", "commit": "12ca8e4296ca9693276422e524d7061685556ba0"}
+{"repo": ".", "date": "2025-01-25", "line": "feat: Added logout functionality and improved login form validation", "commit": "bb6bcf41d1bb2132684b6251853f7d34e202a9f7"}
+{"repo": ".", "date": "2025-01-25", "line": "feat: Refactor service and mapper setup, introduce Cache and Object\n\nThis commit refactors the service and mapper setup to utilize a more structured approach with SimpleNamespace and Object. It also introduces a Cache class for caching frequently accessed data and a BaseObject class for managing object attributes. Additionally, new mappers and models for channels, channel members, and channel messages have been added to support the new features.", "commit": "b4f9ff2c628ffd5aafdfbc4a403b2b71fa0110c8"}
+{"repo": ".", "date": "2025-01-25", "line": "feat: Refactor model and service imports for clarity", "commit": "f25feeeca3502eee94554e7152ca7ca946115053"}
+{"repo": ".", "date": "2025-01-26", "line": "feat: Added RPC view and WebSocket support for real-time communication.", "commit": "488afdcc747df9593273f652b17b5fe8db07b1df"}
+{"repo": ".", "date": "2025-01-26", "line": "refactor: Minor code formatting and whitespace adjustments", "commit": "4c601e8333b3a462c63ab6e02b73b9f5306b4a58"}
+{"repo": ".", "date": "2025-01-26", "line": "feat: Implemented basic chat functionality with message sending and display", "commit": "4ae846cf8b4f1158ac47ce2825d37e03e9b6677f"}
+{"repo": ".", "date": "2025-01-26", "line": "feat: Use dynamic websocket URL based on environment", "commit": "fb7cb35921b73fd22a4ef045fe23b8dab87a7af4"}
+{"repo": ".", "date": "2025-01-27", "line": "feat: Improve WebSocket connection handling and UI\n\nThis commit introduces several improvements:\n\n-   Added error handling to WebSocket send operations in `SocketService`.\n-   Updated the WebSocket URL in `app.js` to include the port number.\n-   Implemented `query` methods in `BaseMapper` and `BaseService` for database queries.\n-   Added a `chat-window` component and updated the HTML structure for a better chat interface.\n-   Implemented `get_messages` method in `RPCView` to fetch messages from the database.\n-   Added `get_channels` method in `RPCView` to fetch channels.", "commit": "36c69eb8bb35068faebd396af1375fe5927eec44"}
+{"repo": ".", "date": "2025-01-27", "line": "feat: Initial chat window component with channel loading", "commit": "87895a72d3ddb5f3ca98e4409f251e663e6dd688"}
+{"repo": ".", "date": "2025-01-27", "line": "feat: Added snek.d* to .gitignore and configured database path", "commit": "aec9ffd1a1a49acad8940b793be6ff3abcae07a3"}
+{"repo": ".", "date": "2025-01-27", "line": "feat: Added notification service and related mappings and services.", "commit": "4f71f745744b1a413a729875bc42366ea3ab665d"}
+{"repo": ".", "date": "2025-01-27", "line": "feat: Implement chat input component with basic functionality", "commit": "2a3e225e1dbb40374e841af8977ff19cd4711f0c"}
+{"repo": ".", "date": "2025-01-27", "line": "fix: Use user uid from database after login", "commit": "188a1e61783a7d08cb7ece1fbcd332aa1f19672a"}
+{"repo": ".", "date": "2025-01-27", "line": "fix: Disable debug logging and remove unnecessary console statements", "commit": "26210f8c09c81f4ff4f7ed796d5d8bcd6d8b639e"}
+{"repo": ".", "date": "2025-01-27", "line": "fix: Removed console logs and initial message", "commit": "095e30a92f6d12edf16ca87d66b335088b853490"}
+{"repo": ".", "date": "2025-01-27", "line": "chore: Remove unnecessary benchmark script", "commit": "374db23669e203c98e5335b9a7abe9aff2110537"}
+{"repo": ".", "date": "2025-01-27", "line": "feat: Improved cache logging and socket cleanup", "commit": "f3d12a257e7a43e3292654d7f67f05d823f16283"}
+{"repo": ".", "date": "2025-01-27", "line": "chore: Remove generated pycache file", "commit": "8e825a90c6e575f114b380312bb9c5726577b8b7"}
+{"repo": ".", "date": "2025-01-28", "line": "feat: Limit table results to 30", "commit": "01d8093e7210910016ea5d6d8bbc5d8f2514c14d"}
+{"repo": ".", "date": "2025-01-28", "line": "feat: Limit message retrieval to 30 entries", "commit": "d93d48ef7e023c62bfa9b64ede20cd9f86c3242e"}
+{"repo": ".", "date": "2025-01-28", "line": "refactor: Improved message handling and added scheduling for event dispatch", "commit": "da72a15068fe14eeb2b50b4cd3342fb4b70b0c79"}
+{"repo": ".", "date": "2025-01-28", "line": "feat: Added schedule functionality and minor UI adjustments", "commit": "99d335ac244c2258d82821344fa517857a782f4a"}
+{"repo": ".", "date": "2025-01-28", "line": "feat: Added schedule benchmarking with custom messages", "commit": "4f1a48c197fcad25d80873bac55cf66f7ff99382"}
+{"repo": ".", "date": "2025-01-28", "line": "feat: Added notification sound on new message", "commit": "5aee606d5d65e71afa8366d24ed4632f662a9126"}
+{"repo": ".", "date": "2025-01-28", "line": "feat: Added notification sound and improved chat input functionality", "commit": "14c59ba5c0abc7d1331e022cc99222223ea21526"}
+{"repo": ".", "date": "2025-01-28", "line": "feat: Improved connection handling and PWA support", "commit": "b2ca373081bdd7514b0f849dc1033edfd3f76424"}
+{"repo": ".", "date": "2025-01-28", "line": "feat: Added favicon and manifest for PWA support", "commit": "7d05bd9da45489c02a9b057eef86d45e2ca90049"}
+{"repo": ".", "date": "2025-01-28", "line": "docs: Added display and start_url to manifest", "commit": "4da635502bca60efd0cc59aa4df236d7b99c2ec2"}
+{"repo": ".", "date": "2025-01-28", "line": "refactor: Reduced padding in chat messages", "commit": "d69c75c6197e857ad61e4dbc872b5ab5872c4837"}
+{"repo": ".", "date": "2025-01-28", "line": "feat: Add data attributes to message elements", "commit": "9e94210bc3f3b1b614a198591c52f404d84a8be2"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Added user color and updated message display", "commit": "84e5bac1b93d5d1c124d303e6b08a29baaf4977c"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Add utility service for generating random light hex colors", "commit": "284d38096c7c5b1201f261ec7a5a28ed457952b5"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Added avatar color and text color", "commit": "93b2f6cc41f08e21241642976b90e3dd98dc37ec"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Add linkify functionality to message text", "commit": "9f652ece1bf0498f9032f94b77becc96b6eff009"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Increased result limit to 60", "commit": "16afbb4e15f370babeedfc2aa917daa0292da5a6"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Hide avatar and author until switch-user message", "commit": "41927b7ef439424326cc58e3939f476e04b8eabb"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Add padding and switch-user class for message differentiation", "commit": "75ec590be5fc3f446c97549d90c135966142ac25"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Show message time on last message and switch user messages", "commit": "0e821f8b588def99f950fecb9369456cff086e0b"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Ensure HTTPS for external URLs", "commit": "931aae5134cad80bf7f5ba87fe215a03761f081b"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Added html-frame.js script tag", "commit": "c558dc2d79b90e7424cf4311747f077332b0a193"}
+{"repo": ".", "date": "2025-01-29", "line": "fix: Corrected typo in script source path", "commit": "5f3dac8bc6b702735383688de44ad7609264742a"}
+{"repo": ".", "date": "2025-01-29", "line": "fix: Ensure URL is HTTPS and handle relative URLs", "commit": "4442f75ec50d3d27cfae1702459d5f8f34ba415b"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Added padding and URL handling for HTMLFrame", "commit": "030942db0984ac0f3a4072581d58d81fad03ef91"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Added install prompt and button for PWA installation", "commit": "438fad301447e3265ff7484606f8222b271e4d9d"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Added install button to navigation", "commit": "3e4b6b00620f8cf2c8b8c63918e6c93d2987174d"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Hide install button initially", "commit": "1f5dc57d6f24b17fa66ab5692038e005cc444378"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Render messages with HTML for rich formatting", "commit": "03c72e85f72207a7b2480f881f7b0cb7055c5feb"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Added message template with markdown support and styling", "commit": "561a915e30274d8b191678135912313ebccde70f"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Refactor project to snekbak", "commit": "d7c003c4096f8cfed8f4edd517f41d45f4f8b501"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Refactor project from snekbak to snek", "commit": "f9fed90e861d8bc5ae5bcd89cb07bd67a1e66a98"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Added documentation and initial form API support", "commit": "b562d171674c2f75592ff3a0dd25b51d2a2457db"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Added cache directory to .gitignore", "commit": "82de0f304469e6214169a2bdcf9c65673baa9e76"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Added docks to message template", "commit": "80f1bbc05e612c45fb2ccbb629a6aa3b468c627e"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Added snek image", "commit": "9e89e27c6688b0e05e4a10a0538d599f82278e64"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Added notification audio and scheduling functionality", "commit": "af399e3b72c772ed97e943e7d71dc6384ab8ccc0"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Added Docker configuration for deployment", "commit": "3be25285f4f0afaaf991ee7cc0a8f71854e8de4c"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Enable markdown rendering in message display", "commit": "75cb7605cd5e8e91cab2ffbc9000eb5987e40136"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Render markdown messages with raw HTML support", "commit": "4fbfe90a1309ec7bf7bf1d19465a3fc441aaddc5"}
+{"repo": ".", "date": "2025-01-29", "line": "chore: Remove compiled python files", "commit": "f69586ccf7975be0bdd24659d6acec068f5183d6"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Added requests dependency and template extensions for linkification and python execution.", "commit": "bca39a612cad5f340864a4dc62d94cda962985f9"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Style message images and add time ago functionality", "commit": "5b88350ff27b526c5e4ee938d0665d3a4e1b5b5c"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Improve time display and linkify URLs in messages", "commit": "20d8d27f03e87bf06515d0664a00e669b92df49f"}
+{"repo": ".", "date": "2025-01-29", "line": "fix: Prevent linkify_https from processing non-https text", "commit": "3d6e1d2e943baabaf0b0875284bf18132bc3967a"}
+{"repo": ".", "date": "2025-01-29", "line": "refactor: Removed unused padding rule in base.css", "commit": "5c4c5793899776e5d369f3949b4a8142a68ba7ee"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Display install button inline-block", "commit": "a8e3ad1af9f683ad25730ff48180c5306f72e1f6"}
+{"repo": ".", "date": "2025-01-29", "line": "feat: Added username to chat messages", "commit": "99cea506de5ea0c5b373869b7c28d965b8af55e6"}
+{"repo": ".", "date": "2025-01-29", "line": "fix: Corrected calculation in timeAgo function", "commit": "03e90039695abc0fdc9276980bd8728bd8951f05"}
+{"repo": ".", "date": "2025-01-31", "line": "feat: Added SSH service and basic gallery styling", "commit": "b06a10f6eca08f312c4f53fac36a4a8dcd9d91b5"}
+{"repo": ".", "date": "2025-01-31", "line": "feat: Initialized SFTP server with basic authentication and file serving.", "commit": "15de277a5be330fe6962e5271c537e3b5ef40de4"}
+{"repo": ".", "date": "2025-01-31", "line": "feat: Enable unbuffered python output", "commit": "8eff6dd6cb7a8ccf866f8f98d22d3aea59c572f6"}
+{"repo": ".", "date": "2025-01-31", "line": "feat: Added logging for response messages", "commit": "4de93489ef01bf070f461915989be611156121dd"}
+{"repo": ".", "date": "2025-01-31", "line": "fix: Improve socket error logging and flushing", "commit": "1c53a90e00bd5ec8eacfeaeb386516cb470b8b3d"}
+{"repo": ".", "date": "2025-01-31", "line": "feat: Added logging for incoming websocket messages", "commit": "312b9eeecaee5d16247a7f0c694e3168893c389d"}
+{"repo": ".", "date": "2025-01-31", "line": "fix: Simplify error handling in RPCView", "commit": "c6f43931664c01c597c642e35c64ec49f3008101"}
+{"repo": ".", "date": "2025-01-31", "line": "refactor: Remove debug print statement", "commit": "5fd03efc301d722a5ee09c8b0cef6d04c1130fd3"}
+{"repo": ".", "date": "2025-01-31", "line": "fix: Handle WebSocket close events and improve error handling", "commit": "780c178d95a6dbe3fbd6b2fac18a6bdb16ec0b64"}
+{"repo": ".", "date": "2025-01-31", "line": "fix: Added logging for RPC exceptions", "commit": "cc3b896d2cd80affc251434800844829cc3fb6e1"}
+{"repo": ".", "date": "2025-01-31", "line": "refactor: Use internal send method for RPC responses", "commit": "0a70e80668a598c909674c78b654b5ad8e6afce5"}
+{"repo": ".", "date": "2025-01-31", "line": "refactor: Buffered RPCView methods", "commit": "bfdfa6c8bb27be4bd83bf8d4e3084e37ef0f7fae"}
+{"repo": ".", "date": "2025-01-31", "line": "fix: Use `send_str` instead of `send_text` for JSON serialization", "commit": "010f3b03a0983843c74219484e78c50d595da6e7"}
+{"repo": ".", "date": "2025-01-31", "line": "feat: Import json for RPC handling", "commit": "8f502af84eea60b5349fd1980d352f0f8e001502"}
+{"repo": ".", "date": "2025-01-31", "line": "fix: Ensure messages are flushed to console", "commit": "10c7232a8f6378eed8f5b4adecca8d582d57a069"}
+{"repo": ".", "date": "2025-01-31", "line": "fix: Prevent premature socket deletion on error/close", "commit": "88749ce05c7c4e9b5e238d16cb9fa4053f092fc1"}
+{"repo": ".", "date": "2025-01-31", "line": "fix: Unbuffered websocket subscriptions", "commit": "2ae2e8450cad47031067f3baae3b09ff521c5c87"}
+{"repo": ".", "date": "2025-01-31", "line": "fix: Removed unnecessary style block in message template", "commit": "495543144d464121af0afab6545a5267ad561a57"}
+{"repo": ".", "date": "2025-01-31", "line": "feat: Added highlight styles to web.html", "commit": "cfd3e7881eca77d10d32de2440a9d2b03aeaea96"}
+{"repo": ".", "date": "2025-01-31", "line": "refactor: Streamlined message template rendering", "commit": "efe12644eda127170a3d60e086fa31ed940fca6e"}
+{"repo": ".", "date": "2025-01-31", "line": "fix: Prevent default event on keyup and change events", "commit": "7526bcc816ffb759e3708f30167b4d3367955b64"}
+{"repo": ".", "date": "2025-01-31", "line": "fix: Prevent form submission on Shift+Enter in chat input", "commit": "1999a6c8d8dd4fdbd48d5553a1704dfa065275ee"}
+{"repo": ".", "date": "2025-01-31", "line": "fix: Prevent empty messages from being submitted", "commit": "ae5fffe5e0faf948a22feea0e651e08a0ed559fb"}
+{"repo": ".", "date": "2025-01-31", "line": "fix: Prevent submitting empty messages", "commit": "663ab415101e5da31fb71e3e9e3b433fbd6c3031"}
+{"repo": ".", "date": "2025-01-31", "line": "feat: Added query endpoint with security checks", "commit": "3796c7c54767b5de18c5310d20c9dd3c5aafdd0c"}
+{"repo": ".", "date": "2025-01-31", "line": "feat: Made channel query asynchronous", "commit": "f6f99684307249b6650dcbbb3168db1ebfa71e73"}
+{"repo": ".", "date": "2025-01-31", "line": "fix: Disable autoescape before linkify and markdown", "commit": "0c68c4e62255a307ecb48cba011ef38ace935eb3"}
+{"repo": ".", "date": "2025-02-01", "line": "feat: Allow underscores and plus signs in usernames", "commit": "4185bb3a69ac66d7b6614acf76bb5a2f613e0b82"}
+{"repo": ".", "date": "2025-02-01", "line": "feat: Added emoji support to templates and app.py", "commit": "928969b8b6266298317ea4f7ca3e6b2cfbd42e82"}
+{"repo": ".", "date": "2025-02-01", "line": "refactor: Removed unnecessary timezone handling", "commit": "feeb94c9cf08ebee6d42165988b1d51030df4c33"}
+{"repo": ".", "date": "2025-02-01", "line": "feat: Added highlight stylesheet link", "commit": "e0ed4491b414c51b54e4c3ebd10cbebb46a903c6"}
+{"repo": ".", "date": "2025-02-01", "line": "feat: Add basic syntax highlighting CSS", "commit": "98d89dbc5f45a61ab6335e38d6e4a1df39bcc621"}
+{"repo": ".", "date": "2025-02-01", "line": "feat: Applied highlight.css to message templates", "commit": "a06e3f404a15d8115fa65ba8533ff7774baa0beb"}
+{"repo": ".", "date": "2025-02-02", "line": "feat: Add user data and audio notification check", "commit": "99fc9118b37f8564cd6e211d3d77ef997592f361"}
+{"repo": ".", "date": "2025-02-02", "line": "fix: Resolve issue with user data initialization", "commit": "7d750db1f8235c8231699c2da39c1075ac678841"}
+{"repo": ".", "date": "2025-02-03", "line": "feat: Improved code display with word wrapping and line breaks", "commit": "3ae43c84e768a712fd2d0a8e65f52edd86bfa6a5"}
+{"repo": ".", "date": "2025-02-03", "line": "style: Improved text wrapping in base.css", "commit": "23c8ebca73ac49c826434d40fd1e1fd2e3435957"}
+{"repo": ".", "date": "2025-02-03", "line": "style: Improved text wrapping in message content", "commit": "38a24e9a12355f93776c9aef0b9caa5afb075531"}
+{"repo": ".", "date": "2025-02-03", "line": "feat: Commented out padding in message list for testing", "commit": "83cc0f613708ed27b928ba45b330c984d82dd546"}
+{"repo": ".", "date": "2025-02-03", "line": "feat: Added padding to message list and hid avatar", "commit": "079187e1b460e5554bfec8b9658b5059cc3d51c6"}
+{"repo": ".", "date": "2025-02-03", "line": "feat: Hide avatar on message list", "commit": "f4a5536dcf1e27a7e8319488f8f39a8acfb818a2"}
+{"repo": ".", "date": "2025-02-03", "line": "revert: Restored avatar visibility", "commit": "fe707dca4ea0bc2ecaedcda292f1ae636fce2b93"}
+{"repo": ".", "date": "2025-02-03", "line": "feat: Added upload button functionality to the chat interface.", "commit": "b48a901e3385617d36511e251c4e7c62498e23bc"}
+{"repo": ".", "date": "2025-02-03", "line": "fix: Remove unnecessary whitespace and improve text wrapping", "commit": "f395d1617394045cac7c41af0cd5ce9d6ef55ed8"}
+{"repo": ".", "date": "2025-02-03", "line": "refactor: Removed setup.cfg and adjusted code for improved stability", "commit": "084f8dba2075aec93d9d88fd7cdd7f67fc63a212"}
+{"repo": ".", "date": "2025-02-04", "line": "feat: Added drive service with upload functionality", "commit": "6f9adfe67fd551dd99746c40bb55706a7ffcef3b"}
+{"repo": ".", "date": "2025-02-05", "line": "feat: Added :snek1: emoji support", "commit": "b6185a95f3fcbf539ec0ba767d4c0923092f8e82"}
+{"repo": ".", "date": "2025-02-06", "line": "fix: Handle failed RPC calls and track success status", "commit": "203314b209030f297cd888685bb68721bc21c61b"}
+{"repo": ".", "date": "2025-02-07", "line": "fix: Correctly detect code language or fallback to bash", "commit": "386d9c3aaee80115241866ae72df9fad3ea3c714"}
+{"repo": ".", "date": "2025-02-07", "line": "fix: Handle empty code highlighting results", "commit": "d4aaa2d66be0a568eff8caf5ecef3c5826e6c67e"}
+{"repo": ".", "date": "2025-02-07", "line": "fix: Escape code blocks in markdown renderer", "commit": "a301e2c5bfb8286f63a48c2860162780f95e820d"}
+{"repo": ".", "date": "2025-02-07", "line": "fix: Updated html.parser import", "commit": "cfa2af61b81b613bf2b8177b8acff1ea8b7c8576"}
+{"repo": ".", "date": "2025-02-07", "line": "feat: Add code highlighting with lexer selection", "commit": "51f1b1d86e4813c10e2750f0771c1bdcc1274bfb"}
+{"repo": ".", "date": "2025-02-07", "line": "feat: Replaced navigation links with emojis", "commit": "9840c8eb03f969330583e9c3a7b28dbb5548f7d6"}
+{"repo": ".", "date": "2025-02-07", "line": "fix: Prevent lingering websocket connections on disconnect", "commit": "7ca2bc5776213828a31c7fc237784a0a73c6f759"}
+{"repo": ".", "date": "2025-02-08", "line": "fix: Updated username regex and added user search functionality", "commit": "f291c0f2e4081fde4ed55d6cc25fdcbb1952af70"}
+{"repo": ".", "date": "2025-02-08", "line": "feat: Initial app template with basic structure and links", "commit": "fcb05903f3f583ce8532d65ee7edf1ad8df91df4"}
+{"repo": ".", "date": "2025-02-08", "line": "fix: Relaxed username and password regex constraints", "commit": "8d0d709e18be0177b99f76f320eeb02b70bb41b0"}
+{"repo": ".", "date": "2025-02-08", "line": "feat: Added user search functionality with HTML and API endpoint", "commit": "d7b943dc8c8f485c975730d6054e32e67db36c91"}
+{"repo": ".", "date": "2025-02-08", "line": "feat: Implemented search user form", "commit": "49eb76dc8b93cd422a9fda40cece480a573b8524"}
+{"repo": ".", "date": "2025-02-08", "line": "feat: Prevent potentially harmful queries via search form", "commit": "60ca3ec7918073a2fb3ebe81e9ea733225391d99"}
+{"repo": ".", "date": "2025-02-08", "line": "feat: Style updates and search user form improvements", "commit": "5154811b29ced87375ac457fafeb25305f64a954"}
+{"repo": ".", "date": "2025-02-08", "line": "style: Adjusted chat message container styling", "commit": "a8fea31a326c7b9868dde866be553bb9f84eee88"}
+{"repo": ".", "date": "2025-02-08", "line": "feat: Optimize database performance with WAL mode", "commit": "06b539b8845c49ec6d2789876ef4069b8df77117"}
+{"repo": ".", "date": "2025-02-09", "line": "feat: Prevent text selection in UI elements", "commit": "b169fa4792e02303ea0f61e5d83b3993a8f72f05"}
+{"repo": ".", "date": "2025-02-09", "line": "feat: Prevent text selection on title and logo elements", "commit": "ad4847a78e2945fe4f57ae16262e0c2a91a804f5"}
+{"repo": ".", "date": "2025-02-09", "line": "fix: Prevent text selection in navigation", "commit": "bda5cfd52d5272742d147e93b506b87eeee04e1e"}
+{"repo": ".", "date": "2025-02-09", "line": "feat: Refactor search user view and template name", "commit": "afa40ada778c5d4102bce19312456adca51f70d0"}
+{"repo": ".", "date": "2025-02-09", "line": "feat: Implemented user search functionality with basic UI", "commit": "a42c2bdf5d2cee53c16e8dc123fc4473107ef203"}
+{"repo": ".", "date": "2025-02-09", "line": "feat: Prevent SQL injection by enhancing query validation", "commit": "e2a8efe5caac1ffaa70d6d7dc55e4e6b9741a35f"}
+{"repo": ".", "date": "2025-02-09", "line": "feat: Added branding and updated view templates", "commit": "a3cec5bce0386c8c3012262aa4a582241786220d"}
+{"repo": ".", "date": "2025-02-09", "line": "feat: Added WebView and channel routing\n\nfix: Corrected form validation in LoginView", "commit": "78f9679f308016320b64cd49bb3552fb63d26d27"}
+{"repo": ".", "date": "2025-02-09", "line": "feat: Added time descriptions and user switching in chat window", "commit": "e7cd397e0fe98074833e08880d915516718adaf5"}
+{"repo": ".", "date": "2025-02-09", "line": "feat: Added custom scrollbar styling for chat messages", "commit": "feb5234b3b581936d45ac328b23de7da8f375ee2"}
+{"repo": ".", "date": "2025-02-09", "line": "refactor: Improved chat message rendering and layout updates", "commit": "ecb77cf361f0d55b512028701c67c1e347836e6e"}
+{"repo": ".", "date": "2025-02-09", "line": "feat: Improved layout updates after message insertion", "commit": "bfca2bdf734c9b9522186c1ff1b6479f93f34658"}
+{"repo": ".", "date": "2025-02-09", "line": "feat: Refactor CSS for improved layout and styling", "commit": "83121f7fa99df690a3b9029556aa023226cf22ef"}
+{"repo": ".", "date": "2025-02-09", "line": "fix: Correctly append message element to chat messages", "commit": "dc2a31abeec3a85dab3c29ec270ae9fcf5ff2797"}
+{"repo": ".", "date": "2025-02-09", "line": "style: Improved text wrapping and code highlighting", "commit": "cef83aefe7a4d2b37b9d4067d7482d9660a2dcbd"}
+{"repo": ".", "date": "2025-02-09", "line": "feat: Improved message appending in chat", "commit": "a6555dc069b81c25ea6bd3f8f3e6132cb2a2ea29"}
+{"repo": ".", "date": "2025-02-09", "line": "feat: Improved message content styling", "commit": "661eba7161c1d869d73f04641878521fbdf8b72a"}
+{"repo": ".", "date": "2025-02-09", "line": "style: Removed unnecessary block display from message content.", "commit": "e75836fe879e4f7e2a8bb34ed8ca901cc624ce05"}
+{"repo": ".", "date": "2025-02-09", "line": "feat: Improved message time display", "commit": "0f400a0b6aa04ffdfd8de1e26be3318a39a174a8"}
+{"repo": ".", "date": "2025-02-09", "line": "feat: Enable CORS credentials", "commit": "49c0f932ab3e5705380a57cefa8da9ea7b9967d3"}
+{"repo": ".", "date": "2025-02-10", "line": "feat: Implemented online status and ping functionality", "commit": "54c40c6b8586fbb9b2b639cd0c7aa1c72a6e53f1"}
+{"repo": ".", "date": "2025-02-10", "line": "feat: Display online status for channel members", "commit": "688e7fbf0e8977f442edc41cea1ac2a06f1ece40"}
+{"repo": ".", "date": "2025-02-10", "line": "feat: Increase online status timeout to 20 seconds", "commit": "48891c438694d37cd1b8338a2cc1f96f7647e77d"}
+{"repo": ".", "date": "2025-02-10", "line": "feat: Include last ping in online user data", "commit": "087ab1a8a55ae58b52078dc5cb7de7db65132e84"}
+{"repo": ".", "date": "2025-02-10", "line": "feat: Implement online user status check", "commit": "3f75c8d5f9a67e68cf311ac5b9c60f13a3aa6493"}
+{"repo": ".", "date": "2025-02-11", "line": "feat: Added private chat functionality with DM creation", "commit": "8a59ddd210bb3ab3d29f9207afbf887988b528d9"}
+{"repo": ".", "date": "2025-02-11", "line": "feat: Implemented private chat functionality", "commit": "ca463b79a88687a76d9fab851b8f2ffc7e071e81"}
+{"repo": ".", "date": "2025-02-11", "line": "feat: Implemented private chat functionality and updated tag to lowercase", "commit": "8fe24f711cb3e796471145a086c4b72289e12e1a"}
+{"repo": ".", "date": "2025-02-11", "line": "feat: Implement private chat redirection", "commit": "bfe4b351c1fa750c9d12d3ae880928cca0346bba"}
+{"repo": ".", "date": "2025-02-11", "line": "feat: Implement private chat redirection", "commit": "be35a6caf07c51eaf79625ad914215bebb9b11c5"}
+{"repo": ".", "date": "2025-02-11", "line": "feat: Added base URL property and file type handling for uploads", "commit": "2cfb8fe3085f6c592488e81b3acf2dbb6f0ac420"}
+{"repo": ".", "date": "2025-02-11", "line": "feat: Display uploaded files as links instead of iframes", "commit": "2541fc536aa45630ec55297c58787686e4154fab"}
+{"repo": ".", "date": "2025-02-11", "line": "feat: Added echo endpoint and noresponse return value", "commit": "b6eba608435be4d798e50328f9149a8768a5cc8e"}
+{"repo": ".", "date": "2025-02-13", "line": "feat: Added channel list and updated templates", "commit": "3baa6e53df459c3816958fbcd4cc6d4bbd1a8fd0"}
+{"repo": ".", "date": "2025-02-13", "line": "feat: Add channel list and improve DM user display", "commit": "37da903936e4ab85fae254421c356966991d53e4"}
+{"repo": ".", "date": "2025-02-15", "line": "```\nrefactor: Improved socket communication and removed unnecessary prints\n```", "commit": "1f8ebf71d0c2f7f1460ba7a1b6113831e4148edb"}
+{"repo": ".", "date": "2025-02-15", "line": "feat: Refactor socket handling and messaging for improved user management and broadcasting", "commit": "53be4b060a1fff9cf58c7224dc4522bb0cafa852"}
+{"repo": ".", "date": "2025-02-15", "line": "feat: Add channel tag to RPC view data", "commit": "9c3abdec2613c4d492c363cca8a07882dd3d8135"}
+{"repo": ".", "date": "2025-02-15", "line": "fix: Correct channel UID retrieval in RPCView", "commit": "d1396801c05688e15ad7f1082dab2576b9a2b011"}
+{"repo": ".", "date": "2025-02-16", "line": "feat: Embed YouTube videos directly into the page", "commit": "7c4334fe7b5b7e6ba44627a4f084638af51dd44c"}
+{"repo": ".", "date": "2025-02-16", "line": "feat: Embed media, images, and YouTube videos in links", "commit": "c463dc6dca38348f9a54189e0b6eff6f5a3eb9b2"}
+{"repo": ".", "date": "2025-02-16", "line": "fix: Correctly linkify HTTPS URLs", "commit": "263595fc7e7f86ec5d34b967b52c3d0a57dbc5fc"}
+{"repo": ".", "date": "2025-02-16", "line": "refactor: Remove unused YouTube embed code", "commit": "7bcc67c6d35484c0fb8ddf201ea5b23b533d99d3"}
+{"repo": ".", "date": "2025-02-16", "line": "fix: Handle file extensions in upload URLs", "commit": "be956a13db0941008802701196ca5e3870ebf2aa"}
+{"repo": ".", "date": "2025-02-17", "line": "fix: Ensure images, videos, and iframes within messages are responsive", "commit": "ea4196af8f7d7e0c97c004a07817bdc1dac999f5"}
+{"repo": ".", "date": "2025-02-17", "line": "fix: Improved media handling in message content", "commit": "f28be3ba55cbe9c1b20f70b4e1e8e2668eb2388f"}
+{"repo": ".", "date": "2025-02-17", "line": "feat: Implement scroll to bottom on new message", "commit": "2e69ac5921c16ea0cda1a1b7c84dd63ff458db62"}
+{"repo": ".", "date": "2025-02-17", "line": "feat: Implement timestamped pagination and focus textbox.", "commit": "8c33bc63d6cc623d0782f14c96a42c612accbc75"}
+{"repo": ".", "date": "2025-02-17", "line": "feat: Implement infinite scrolling for chat messages", "commit": "477ca5917a59d3720a6a5ae01b307dec1a74cfaf"}
+{"repo": ".", "date": "2025-02-17", "line": "feat: Implement infinite scrolling for chat messages", "commit": "9e3b9ae326b6ec632f3280018ea68d1896645b9a"}
+{"repo": ".", "date": "2025-02-17", "line": "fix: Use offsetMessage timestamp for infinite scroll", "commit": "33bc695cda6bf5889d802129117ed59992b87143"}
+{"repo": ".", "date": "2025-02-17", "line": "fix: Corrected method name for fetching messages", "commit": "aa5703e62f891fa7db09f07e6ce060875f5990d3"}
+{"repo": ".", "date": "2025-02-17", "line": "fix: Corrected offset calculation for infinite scroll", "commit": "1792686531fb440d9f453422bfa648c890c255d1"}
+{"repo": ".", "date": "2025-02-17", "line": "fix: Adjust offset for infinite scroll", "commit": "3ee7c6d8024245933569fdaf3f99a71afd14fe8f"}
+{"repo": ".", "date": "2025-02-17", "line": "feat: Implement infinite scrolling for chat messages", "commit": "95a8a458420dd2ebdbd6f7c03bd58c649985933e"}
+{"repo": ".", "date": "2025-02-17", "line": "feat: Implement infinite scrolling for messages", "commit": "162f89f9d0f7e304d355dd8f626ce2430dd840bf"}
+{"repo": ".", "date": "2025-02-17", "line": "fix: Use first message timestamp for infinite scroll", "commit": "104ee277669ee2cd55eb97b674f7a2432d31bb5a"}
+{"repo": ".", "date": "2025-02-17", "line": "fix: Prevent loading extra messages when not scrolled past half", "commit": "60efe6ee8a158cd671cbd05c20c7d382d6dcbb3b"}
+{"repo": ".", "date": "2025-02-17", "line": "feat: Implement infinite scrolling based on scroll position", "commit": "2fb6be753efa7f2cc1e0183551a1ad655388b970"}
+{"repo": ".", "date": "2025-02-17", "line": "feat: Hide scrollbar in chat messages", "commit": "24cd378c9d8f857f4f28af1069a2f5523a6441d8"}
+{"repo": ".", "date": "2025-02-17", "line": "fix: Use scroll instead of auto for chat messages", "commit": "8b98935d11496d51a0007db4b78391dde7a69163"}
+{"repo": ".", "date": "2025-02-17", "line": "feat: Hide scrollbar and improve chat styling", "commit": "5b03ecda3f3f9ae515dd00a4e421255535a2f215"}
+{"repo": ".", "date": "2025-02-17", "line": "feat: Hide scrollbar in chat messages", "commit": "3230c9f93bf9c3ae1b27474eac1cfc35f626d387"}
+{"repo": ".", "date": "2025-02-17", "line": "fix: Improve infinite scroll trigger position", "commit": "6c58f4b26c628881aee5cbe0597ba489f705e42f"}
+{"repo": ".", "date": "2025-02-17", "line": "fix: Adjust scroll position threshold for infinite scrolling", "commit": "bc8a296223f3a2c6e07b126a78373aa5bb40399d"}
+{"repo": ".", "date": "2025-02-17", "line": "fix: Adjusted scroll position check for infinite scrolling", "commit": "c77d2fb782258f787c5b52e3b27c5a3b0d468903"}
+{"repo": ".", "date": "2025-02-17", "line": "fix: Adjusted scroll threshold for infinite scrolling", "commit": "2e86ca2a3f1a4a8c746eecb46038a216b9706cdf"}
+{"repo": ".", "date": "2025-02-17", "line": "fix: Disable extra scroll loading", "commit": "1a608d8cfb1a3dcef9591b214fc54904615148bf"}
+{"repo": ".", "date": "2025-02-17", "line": "fix: Enable infinite scroll when near the bottom", "commit": "f0d76bd46af06637a21526c58d97f4b4d57f87dd"}
+{"repo": ".", "date": "2025-02-17", "line": "feat: Disable scroll loading for now", "commit": "1b6ebf50080b0b86256e639031f36da92b8990b2"}
+{"repo": ".", "date": "2025-02-17", "line": "fix: Corrected infinite scroll trigger condition", "commit": "6bdc6a7347a492f155629458c9b277cc16e04666"}
+{"repo": ".", "date": "2025-02-17", "line": "fix: Corrected infinite scroll logic", "commit": "c042af8b800879ecfbb817089119aab75d839c32"}
+{"repo": ".", "date": "2025-02-17", "line": "feat: Implement infinite scrolling for messages", "commit": "bb2b4b61b49bf4ba38d75bcbb0d751961c49cfa3"}
+{"repo": ".", "date": "2025-02-17", "line": "fix: Corrected initial message retrieval for infinite scroll", "commit": "2595594c3a99f6613e8e2194977fb1707c9f8b98"}
+{"repo": ".", "date": "2025-02-17", "line": "refactor: Added comments and improved message loading logic", "commit": "6555e4f8266b01963cfd660a4e175b01ab615c0c"}
+{"repo": ".", "date": "2025-02-17", "line": "refactor: Remove unnecessary comments from web.html", "commit": "2ab4341d0099799a84c0df6d91de33e1c5f69470"}
+{"repo": ".", "date": "2025-02-17", "line": "feat: Update manifest and app.html for PWA support", "commit": "e21880b4f5fd15259e09c207ce54d8e86bd61ac7"}
+{"repo": ".", "date": "2025-02-17", "line": "fix: Removed display_override from manifest", "commit": "6c7266f20403f1f190c8b41f22d653c041dbbc77"}
+{"repo": ".", "date": "2025-02-17", "line": "feat: Added 192x192 icon to manifest", "commit": "c745f609976397de0fb0cf7dec80205239b44b87"}
+{"repo": ".", "date": "2025-02-18", "line": "refactor: Switch to asyncio for application startup and debugging", "commit": "3ccbe8be5c604d2683cf55553d1f11c674f6b930"}
+{"repo": ".", "date": "2025-02-18", "line": "fix: Standardize environment variables in compose.yml and add logging to app.py", "commit": "ebb520dd4a80b513d1eb6fc6ce90e6b46f905100"}
+{"repo": ".", "date": "2025-02-18", "line": "feat: Integrated profiler for performance analysis", "commit": "c6620ad70afce9407c16793de8ab4fea35523d81"}
+{"repo": ".", "date": "2025-02-18", "line": "fix: Sort profiler stats by query parameter", "commit": "91a21db89b6d7bc36b5525f9d3a07d1ebe2a4ad3"}
+{"repo": ".", "date": "2025-02-18", "line": "fix: Corrected typo in profiler sorting", "commit": "60404c6fd31894f3fbb6ce31ba48f1750101748f"}
+{"repo": ".", "date": "2025-02-19", "line": "feat: Added a1 emoji and long emoji", "commit": "736123c4aa313c51a7e0daee8cdd6dc7583547fd"}
+{"repo": ".", "date": "2025-02-19", "line": "feat: Increased Gunicorn workers for improved performance", "commit": "2ad5a7b1f49704baf7b890fcfda7a87fddd456f7"}
+{"repo": ".", "date": "2025-02-19", "line": "fix: Reduced Gunicorn workers to 1", "commit": "e06824f4ec703388b7d55beeb5f1b3ef12452226"}
+{"repo": ".", "date": "2025-02-19", "line": "refactor: Increased Gunicorn workers and moved app instantiation to global scope", "commit": "821db3cb1a67c20a968ac1dd8ecc4263e511cf16"}
+{"repo": ".", "date": "2025-02-20", "line": "feat: Improved database indexing and UI enhancements\n\nThis commit introduces database indexing for improved query performance and several UI enhancements:\n\n- Added indexes to `user`, `channel_member`, and `channel_message` tables.\n- Updated CSS to include a container with improved styling for lists and links.\n- Modified `manifest.json` to set the scope to `/`.\n- Refactored `template.py` to handle image embedding and YouTube links more robustly.\n- Adjusted `app.html` to display \"Channels\" instead of \"Chat Rooms\".\n- Enhanced `search_user.html` with a container and improved styling.\n- Sanitized user data in `rpc.py` to remove sensitive information like email, password, message, and html.\n", "commit": "3623286a9dfba330612c42e579abcca63ab186ed"}
+{"repo": ".", "date": "2025-02-20", "line": "refactor: Reduced gunicorn workers in compose file", "commit": "a7e0e5a3f821d51eb4e2ecde82baeb8ee0e183c7"}
+{"repo": ".", "date": "2025-02-21", "line": "feat: Added sound effects for mentions", "commit": "54920e1545ffc68e2f928d3d042f5f11080f0d41"}
+{"repo": ".", "date": "2025-02-21", "line": "feat: Add notification sounds for different events", "commit": "8ea41bb592b86e2f49b2f838e03006bc04472da5"}
+{"repo": ".", "date": "2025-02-22", "line": "refactor: Moved sidebar channels to separate template and added channel notification", "commit": "fbe95d6631dfac2edc4c8600922020be4e15eccb"}
+{"repo": ".", "date": "2025-02-22", "line": "feat: Add channel sidebar with message counts and highlighting", "commit": "076fbb30fb51ecfb5b15d394c760f55dac26e1c1"}
+{"repo": ".", "date": "2025-02-26", "line": "feat: Added avatar support and updated message view to display avatars", "commit": "5af4e5754b6902ae13c798d9793281d62b684590"}
+{"repo": ".", "date": "2025-02-26", "line": "feat: Generate unique avatar ID when requested", "commit": "e280e8776457605bbb5548fe9de5328b7b04bb8a"}
+{"repo": ".", "date": "2025-02-26", "line": "feat: Generate unique avatars", "commit": "162cfe394558a085894a11cea57b193d6108b90e"}
+{"repo": ".", "date": "2025-02-26", "line": "feat: Updated welcome message and registration buttons", "commit": "da1be6301c79cf76383e6568f91ee23bdf5119f6"}
+{"repo": ".", "date": "2025-02-28", "line": "fix: Disable login requirement for avatar view", "commit": "66b85d146abac25df83edc1975db209b9d43fae7"}
+{"repo": ".", "date": "2025-03-02", "line": "feat: Add last_message_on to ChannelModel and update on message send", "commit": "4620ebb800b5dd848ec28713f1afa20416698922"}
+{"repo": ".", "date": "2025-03-02", "line": "feat: Add index creation with error handling", "commit": "e469e27abfedc0b08b483e0715b4dc9b16240c5e"}
+{"repo": ".", "date": "2025-03-03", "line": "fix: Correctly handle trailing commas in link targets and improve upload view display", "commit": "45e3239cc06cdab0a8e5c1c1ef56593f65e750ea"}
+{"repo": ".", "date": "2025-03-05", "line": "feat: Implement push notifications with service worker", "commit": "c3c94461c295ef4c6219051369472f983267437c"}
+{"repo": ".", "date": "2025-03-05", "line": "feat: Add notification read functionality and unread stats", "commit": "578c182f2707a5f5b7c93f421e2035f7271aa60c"}
+{"repo": ".", "date": "2025-03-05", "line": "fix: Handle missing notification model in mark_as_read", "commit": "580ec5ab0d57a542ab38b25a6c508804d5bcfa21"}
+{"repo": ".", "date": "2025-03-05", "line": "feat: Add new_count to ChannelMember and update notification service", "commit": "84d7b11f24b37cdc41ce9d5bb24be4080af14be9"}
+{"repo": ".", "date": "2025-03-05", "line": "feat: Display new notification count in RPC view", "commit": "d7851e645785fd707a7c7fdc5b6fff036e0c80f7"}
+{"repo": ".", "date": "2025-03-05", "line": "feat: Increment new_count for channel messages and members", "commit": "e1324e99bf06018a804a1a3dcc83f96cde04b1af"}
+{"repo": ".", "date": "2025-03-05", "line": "feat: Increment new_count for channel messages and members", "commit": "afbf53938bd59e1e03dfc011063b27134dd0c054"}
+{"repo": ".", "date": "2025-03-05", "line": "feat: Added new_count to notifications", "commit": "8d3d7327d777ae3150ecaa237e137f8221310dfa"}
+{"repo": ".", "date": "2025-03-05", "line": "feat: Remove unused new_count field from notification model", "commit": "4df6055566d61c769ae1759d81900d138093136e"}
+{"repo": ".", "date": "2025-03-05", "line": "feat: Increment notification count and log insertion", "commit": "dd11c8da5acc5ed97a278a705e7282edc7d50bc5"}
+{"repo": ".", "date": "2025-03-05", "line": "feat: Add logging for notification and mapper updates", "commit": "8f137cf8e6d67a8e73ee221d2f9192b7a3ab431d"}
+{"repo": ".", "date": "2025-03-05", "line": "feat: Allow channel members to be created", "commit": "30fe0bfae7f2bc9badcb16f734655e661fe976ad"}
+{"repo": ".", "date": "2025-03-05", "line": "feat: Return model directly from query", "commit": "edb35b57070a5659e8491a967880d816f2d07697"}
+{"repo": ".", "date": "2025-03-05", "line": "feat: Display username when inserting notification", "commit": "0613f6f54de9247c17b4bae924197ffb4cbd2966"}
+{"repo": ".", "date": "2025-03-05", "line": "feat: Disable banned, muted, and deleted checks in channel member query", "commit": "1807cff67d3a4306f122d7df4436cc88137f299c"}
+{"repo": ".", "date": "2025-03-07", "line": "feat: Implemented threads view with basic message display", "commit": "5b78165fb7e83f7e8a45f790c0f6e5a9fde758a0"}
+{"repo": ".", "date": "2025-03-07", "line": "feat: Added threads view and related model updates", "commit": "a1afaacb2ea6ded2eb14eb6d8fbdaf34708d9568"}
+{"repo": ".", "date": "2025-03-08", "line": "fix: Prevent race condition when reconnecting socket", "commit": "e3afc1ba6e97378688027a60d6d98cc19a519a8c"}
+{"repo": ".", "date": "2025-03-08", "line": "fix: Corrected avatar styling in threads.html", "commit": "37f6725f2f7e36ec03416f191c9d16cd864991ea"}
+{"repo": ".", "date": "2025-03-08", "line": "feat: Display user avatars in threads", "commit": "095be5892db198d0a6356c8700ed0c038e419a29"}
+{"repo": ".", "date": "2025-03-08", "line": "feat: Improve thread avatar visibility", "commit": "9292e3b8f3b64084d6bcc0b13dd42d015f4799d9"}
+{"repo": ".", "date": "2025-03-08", "line": "fix: Improve thread display and opacity", "commit": "24260f9c371ab2d989441e391f513f6460eaa1ec"}
+{"repo": ".", "date": "2025-03-08", "line": "feat: Unified styling for chat messages and threads", "commit": "1b72063a5b972dd726c647b7397f0ced16bd66c2"}
+{"repo": ".", "date": "2025-03-08", "line": "feat: Improved thread display and DM name handling\n\nThis update enhances thread display by adding a name and color, and fixes a bug in DM name retrieval. It also refactors the code for better readability and efficiency.", "commit": "5a72c8cd83d1374ff709556cbf4b4ddbf7a9ab7a"}
+{"repo": ".", "date": "2025-03-08", "line": "fix: Improve message styling and visibility on mobile", "commit": "98c2213a862b253f8f967f428b0b248bbe3a32f7"}
+{"repo": ".", "date": "2025-03-08", "line": "feat: Improved user search display with thread-like layout and real-time updates", "commit": "0a9b66d2f76a2c4418db7149b17729bf8a2dc811"}
+{"repo": ".", "date": "2025-03-08", "line": "feat: Set default page titles and update login/register titles", "commit": "6ecd356cc08e17596ff6b5007c46def2bc17c851"}
+{"repo": ".", "date": "2025-03-08", "line": "fix: Improved form submission and change event handling", "commit": "804556b74d8caa5e3a79a03cd1a8d7870843b898"}
+{"repo": ".", "date": "2025-03-08", "line": "feat: Added meta information to base.html", "commit": "7c22a70722db3fa97a813c30c01c4cd5462138eb"}
+{"repo": ".", "date": "2025-03-08", "line": "feat: Sort threads by last message timestamp", "commit": "8e195a49e3e914a4b241e95378bd9a07611715a8"}
+{"repo": ".", "date": "2025-03-08", "line": "style: Improved input field styling with focus and placeholder transitions", "commit": "c9113ca09500c5b3cc277fb09b9607a505d39f30"}
+{"repo": ".", "date": "2025-03-08", "line": "feat: Added head block to base template", "commit": "62aa15a4b4d6514824378cca73084c9ce2df903b"}
+{"repo": ".", "date": "2025-03-08", "line": "feat: Implemented back button styling and layout", "commit": "ad7eab9717848584369ded6e19babb6a7b9f5b98"}
+{"repo": ".", "date": "2025-03-08", "line": "Revert: Undid auto formatting", "commit": "e91ae4584aaf08cb02b89cdf26b8c43a8c8179b0"}
+{"repo": ".", "date": "2025-03-08", "line": "Merge main", "commit": "dd5a9a23e8e452a03f7080656608617309bf73a5"}
+{"repo": ".", "date": "2025-03-08", "line": "Merge: Tweaks for login/registration and base + image roundness", "commit": "aedfe9aa947dcd2262c825af5a4d977eb298ccb5"}
+{"repo": ".", "date": "2025-03-08", "line": "feat: Sort channels by last message time", "commit": "a219ce4d79a15ef900583ab025fb0da1df79ace3"}
+{"repo": ".", "date": "2025-03-08", "line": "feat: Filter public channels in sidebar and add private channel section", "commit": "d6061cb45b68a7b393b0e35a560d5d7bea4b9478"}
+{"repo": ".", "date": "2025-03-08", "line": "fix: Sort channels by last message, handling null values", "commit": "24a504e3a7383c7a338fbe3ee09411547eed58eb"}
+{"repo": ".", "date": "2025-03-08", "line": "fix: Sort channels by last message time, handling null values", "commit": "11b8f0e744fb9d6b05ce11b7475bb3f51edee96b"}
+{"repo": ".", "date": "2025-03-08", "line": "feat: Implemented form preloading and autofocus on the first input element for login and register pages", "commit": "0266b2a559952d0ff767b251c2921704a6aa1abe"}
+{"repo": ".", "date": "2025-03-08", "line": "fix: Corrected semicolon in loadForm call", "commit": "fd07001983fc3d3015ac7064461c14b8486155e6"}
+{"repo": ".", "date": "2025-03-09", "line": "feat: Preload form and autofocus first input", "commit": "d9ac1813ba8ddad9fb602730cb2cc763aab4bc23"}
+{"repo": ".", "date": "2025-03-09", "line": "fix: Sort threads by last message, handling missing timestamps", "commit": "91d8f3efd16431fe99b0e60927d1f6d9b6587f7e"}
+{"repo": ".", "date": "2025-03-10", "line": "fix: Improved search user page layout", "commit": "c4e3f1fc1f10e4d98fc04e4928c62c88385fbeb8"}
+{"repo": ".", "date": "2025-03-11", "line": "feat: Improve file download experience with suggested filename", "commit": "c6c2766381f75b058fb61f91556788b0720b058b"}
+{"repo": ".", "date": "2025-03-13", "line": "feat: Added reply functionality and improved time display", "commit": "5cfcafe0821b3cceb753b9ddb4076a79f26a88c0"}
+{"repo": ".", "date": "2025-03-13", "line": "feat: Display creation time on container", "commit": "c55927aa9c7e575544901bbee41cb9a9d3c6437a"}
+{"repo": ".", "date": "2025-03-13", "line": "style: Darkened the overall theme.", "commit": "0fad298fc078e2a3ea1afee71ee92b99b83427b0"}
+{"repo": ".", "date": "2025-03-13", "line": "feat: Added padding to links", "commit": "0f950218d6d783c4738966e15b2746c14043c82f"}
+{"repo": ".", "date": "2025-03-13", "line": "feat: Improved reply formatting with markdown and blockquote", "commit": "d8b43dbd08afa8c4498bbc5611dd6e7d61f9b139"}
+{"repo": ".", "date": "2025-03-14", "line": "feat: Increased update interval for times", "commit": "17c9731b9f8be07b247aeed29ba2ab1319e408f0"}
+{"repo": ".", "date": "2025-03-15", "line": "feat: Centralized input styling in shared.css", "commit": "5b70bb9ea5cc6637ac585cf8f04efd4cde0aa621"}
+{"repo": ".", "date": "2025-03-15", "line": "feat: Applied updated input styling across pages", "commit": "752f3df13a548d22646f87a87940dc64e15587f3"}
+{"repo": ".", "date": "2025-03-15", "line": "feat: Refactor app modules and update script types to ES modules", "commit": "a4d79b06c49ec9f605336bf181e98455c8acd460"}
+{"repo": ".", "date": "2025-03-16", "line": "feat: Convert files to modules", "commit": "a9663c8170dd2f925100eaa50e8c0019c5eee683"}
+{"repo": ".", "date": "2025-03-16", "line": "feat: Refactor socket and event handling for improved reliability and structure", "commit": "4a8a614adb5e15cad18414214d30ab83464eae14"}
+{"repo": ".", "date": "2025-03-16", "line": "refactor: Improved code formatting and spacing", "commit": "c9c070c497bb9af3eb5bb9915f221ec00b56b832"}
+{"repo": ".", "date": "2025-03-16", "line": "fix: Reconnected socket on error", "commit": "e62a8554090009c7914b95833066ad46251da01d"}
+{"repo": ".", "date": "2025-03-16", "line": "feat: Changed button colors to dark theme", "commit": "819cf8381c287e2ee88fb1d6fe789e30a1a33eff"}
+{"repo": ".", "date": "2025-03-16", "line": "style: Adjusted upload button background color", "commit": "2ba28c193a826e8c1f9647f06bffb6084166c9f1"}
+{"repo": ".", "date": "2025-03-16", "line": "feat: Added Umami analytics tracking", "commit": "287e10d8aa8feb2590ad48d9412ace74a9432baf"}
+{"repo": ".", "date": "2025-03-17", "line": "fix: Scroll to the end of the message container", "commit": "54416ee84f88064897a824ae2c3a9e0ef2c1ccaa"}
+{"repo": ".", "date": "2025-03-17", "line": "feat: Refactor header layout and logo display", "commit": "39fa8fa0cd7a725edc1f293a7e8d0ea0d5d5bca6"}
+{"repo": ".", "date": "2025-03-17", "line": "feat: Add Promise.withResolvers polyfill and update templates", "commit": "7fa2817f773b47737236f4bb700023bcf8b483f1"}
+{"repo": ".", "date": "2025-03-17", "line": "revert: Removed unnecessary formatting changes", "commit": "965dc930a900a5080e225bb492be2b799daed22f"}
+{"repo": ".", "date": "2025-03-17", "line": "feat: Add Promise.withResolvers polyfill", "commit": "825ece4e7868be28ba03c4fde5149695b0dd9dc5"}
+{"repo": ".", "date": "2025-03-18", "line": "feat: Added dump script for public channels", "commit": "3c6a0944d68ca16250ec9364d7f006b0e7eea6e8"}
+{"repo": ".", "date": "2025-03-18", "line": "feat: Improved dump functionality with JSON output and user information\n\nfix: Removed unnecessary print statements from cache and dump modules", "commit": "70db15bf27c7a8fd8bf112432f02754f01bbb3d7"}
+{"repo": ".", "date": "2025-03-18", "line": "feat: Refactor dump script to output to dump.txt and improve message formatting", "commit": "3960390ec45f427979ebd2b81c1a21666a47e71d"}
+{"repo": ".", "date": "2025-03-20", "line": "refactor: Update session management for user registration and login", "commit": "5ba239caa8928a078362af0e6e2d1a4626bd508d"}
+{"repo": ".", "date": "2025-03-22", "line": "feat: Added terminal access and updated Dockerfile and compose.yml", "commit": "7dcabde2ed0ab699ea3033f7788381e85c352b97"}
+{"repo": ".", "date": "2025-03-22", "line": "feat: Added terminal access and basic shell environment.", "commit": "013d4adce57f4afec5176bbdb5e2225d529ec3b7"}
+{"repo": ".", "date": "2025-03-22", "line": "feat: Added terminal page with Ubuntu option and channels sidebar", "commit": "6e68408ddfd8be2376f7453deac8f63a6bfb93e4"}
+{"repo": ".", "date": "2025-03-22", "line": "feat: Add channel list and user context to template rendering", "commit": "604e27ce10dee59b1b3f6ebd359ee356b085df2a"}
+{"repo": ".", "date": "2025-03-22", "line": "fix: Corrected execute permissions for r script", "commit": "c72b015073347e86f2edf1e67544b1ae31e929b0"}
+{"repo": ".", "date": "2025-03-22", "line": "fix: Disable r executable and add vim and htop to apt install", "commit": "78c631e6c79b4b69b0e202a301b710c4150e6dbe"}
+{"repo": ".", "date": "2025-03-22", "line": "fix: Corrected execute permissions for r script", "commit": "c8461342fd8d260da69c84f27f9e9d13b3430942"}
+{"repo": ".", "date": "2025-03-23", "line": "feat: Introduce ThreadPoolExecutor for asynchronous task handling", "commit": "0bc24e8d2ef4452efc7be5286288a2531908ea55"}
+{"repo": ".", "date": "2025-03-23", "line": "feat: Improve terminal display and handling", "commit": "c2d9af807a95900c4fecd7a1929c8d92393a955d"}
+{"repo": ".", "date": "2025-03-23", "line": "feat: Install git during initial setup", "commit": "c5c160baae67d7e5932963f8501ed7d56dc35c21"}
+{"repo": ".", "date": "2025-03-23", "line": "feat: Updated drive functionality with new views and services\n\nThis commit introduces a new drive feature with the following changes:\n\n-   Added DriveModel and DriveItemModel to represent drive data.\n-   Implemented DriveService and DriveItemService for managing drives and items.\n-   Created DriveView for displaying drive contents as JSON.\n-   Updated Application class to include drive-related setup and routes.\n-   Modified Makefile to use python3.12\n-   Updated src/snek/app.py to include drive view.", "commit": "7b32a7eba4d5944142a3b40616d37b0862087371"}
+{"repo": ".", "date": "2025-03-23", "line": "feat: Add URL to drive items", "commit": "dec2281ac88d151afda016fc01e833dc2f0aa89e"}
+{"repo": ".", "date": "2025-03-23", "line": "fix: Correctly fetch file extension from item", "commit": "1de2c55966c0ddf0fb663b935427d2c005f0fde9"}
+{"repo": ".", "date": "2025-03-23", "line": "fix: Improved terminal session handling and websocket integration", "commit": "529606955a545bc25cf5899f2c79d3660bcefd54"}
+{"repo": ".", "date": "2025-03-23", "line": "fix: Increased websocket thread pool size", "commit": "af4a70e8949bfde704a0499177050fdeab5300d9"}
+{"repo": ".", "date": "2025-03-27", "line": "feat: Display user color in sidebar and update new message count", "commit": "5390b8bdc3dc04645258ed758f3894de00008e80"}
+{"repo": ".", "date": "2025-03-27", "line": "fix: Prevent errors when user data is missing during notification updates", "commit": "877ef7970d5b7bb5f8fd0af35adad5d8d071b14d"}
+{"repo": ".", "date": "2025-03-27", "line": "refactor: Remove debugging prints", "commit": "87c189b3fe897cc15ee6ac311e987bf9fe7811b9"}
+{"repo": ".", "date": "2025-03-27", "line": "refactor: Cleaned up console output by removing unnecessary print statements", "commit": "71a206a19fa6b2a2ae05d886a39d2fa03f2c0f71"}
+{"repo": ".", "date": "2025-03-27", "line": "feat: Added database transaction for notification creation", "commit": "bd5bb5ae65d6cb870d090382b106b705833d4cf1"}
+{"repo": ".", "date": "2025-03-27", "line": "feat: Persist RPC transaction changes to the database", "commit": "13ce09a5c50ddba8956dbeb838f9dd1bcafe184a"}
+{"repo": ".", "date": "2025-03-27", "line": "feat: Added task runner and task queue for asynchronous operations", "commit": "145373399dada2f4cee54af0c99e62d4a27a0f99"}
+{"repo": ".", "date": "2025-03-27", "line": "refactor: Improve task management with asyncio.Queue and error handling", "commit": "e6f702a6b405f1dae0ce719177c6f7bf5b636ad8"}
+{"repo": ".", "date": "2025-03-27", "line": "fix: Remove unnecessary database transaction block in RPCView", "commit": "9d5815ed1028354ac61f2d506530147a02c476e6"}
+{"repo": ".", "date": "2025-03-27", "line": "feat: Added database transaction management for task execution", "commit": "8810679fd8ea2dd6e63b09bf9a4b9af5c2d0fdd2"}
+{"repo": ".", "date": "2025-03-27", "line": "feat: Added task execution time measurement", "commit": "6ad3844f037735e268b42247fcc6e8605cc13f07"}
+{"repo": ".", "date": "2025-03-27", "line": "feat: Asynchronously create channel messages and improve socket broadcast", "commit": "73e8779bdc42ec5f5618fad3d563544d1fad2b69"}
+{"repo": ".", "date": "2025-03-27", "line": "fix: Ensure notification creation after socket broadcast", "commit": "6f043d21390d13d37772c8ae145d7a66b3919529"}
+{"repo": ".", "date": "2025-03-27", "line": "fix: Await task creation in chat service", "commit": "9e50b2a6d3570c4f86fcfca6a5ce59947c0105ec"}
+{"repo": ".", "date": "2025-03-28", "line": "feat: Initialized Ubuntu Dockerfile and terminal environment setup", "commit": "5fbcadad8bad7b8dd7ac3279938ff50db6b5d380"}
+{"repo": ".", "date": "2025-03-29", "line": "fix: Adjusted avatar size in message template", "commit": "fe1b3d6d191176111538f1ba06018e14c44ca8f9"}
+{"repo": ".", "date": "2025-03-29", "line": "```\nfeat: Added webdav support\n\nThis commit introduces webdav functionality to the application.\n\n- Added the `lxml` dependency to the `pyproject.toml` file.\n- Implemented the `WebdavApplication` class in `src/snek/webdav.py`.\n- Integrated the `WebdavApplication` as a subapp in `src/snek/app.py`.\n- Added necessary imports and code modifications to support webdav.\n```", "commit": "29139d5d0c18ad2b0ebd32db9a0b629c45c0a651"}
+{"repo": ".", "date": "2025-03-29", "line": "feat: Initial implementation of WebDAV functionality with basic operations", "commit": "9a923f6bddd73df27af80ef6c8e2313816a07a48"}
+{"repo": ".", "date": "2025-03-29", "line": "feat: Implemented basic WebDAV authentication", "commit": "886d21999c716ee306318796f3f159ba085f9618"}
+{"repo": ".", "date": "2025-03-29", "line": "feat: Use home directory instead of drive for user folders", "commit": "6dca3a96e1cd23cc26d69aeee31ae45e14977bbd"}
+{"repo": ".", "date": "2025-03-29", "line": "feat: Implemented recursive node creation for propfind requests", "commit": "3926b2d837bef181546d826b41821cf90fc755a8"}
+{"repo": ".", "date": "2025-03-30", "line": "refactor: Updated home folder path to drive and removed lock implementation", "commit": "d5917b94540aee206935354f438a6a7f893278ec"}
+{"repo": ".", "date": "2025-03-30", "line": "refactor: Remove unused LOCK and UNLOCK routes", "commit": "8058e4a4b0aee254edc76fa28b2c4336eb393c4b"}
+{"repo": ".", "date": "2025-03-30", "line": "fix: Handle potential errors when creating user home directories", "commit": "2a47c0ba5e7344d44c3acd15d8f3efeb11722677"}
+{"repo": ".", "date": "2025-04-01", "line": "feat: Added settings page with profile, notifications, and privacy sections", "commit": "32e0c959e8589f574d1c46b96d35f49c98721566"}
+{"repo": ".", "date": "2025-04-01", "line": "feat: Improve responsiveness for smaller screens", "commit": "87b48af551d2ed023f77ca57d033ed0079d303f3"}
+{"repo": ".", "date": "2025-04-01", "line": "refactor: Reduced message list height", "commit": "18b1ec20b67522cf816b29f3cde64a935ec5b330"}
+{"repo": ".", "date": "2025-04-01", "line": "feat: Removed chat window element", "commit": "7c52c2d9d5f10623ccf479918ea5baed247a07b5"}
+{"repo": ".", "date": "2025-04-01", "line": "feat: Added footer styling for improved layout", "commit": "fbd4fa4e668628c11d8b592718cfcdcca71a3c0f"}
+{"repo": ".", "date": "2025-04-01", "line": "feat: Style adjustments for mobile view", "commit": "d24627b35fd2201e6baad781a11fbae0c379f366"}
+{"repo": ".", "date": "2025-04-01", "line": "feat: Adjusted width for mobile responsiveness", "commit": "e3c997302b07228c0791d6307b429109b6eb3d53"}
+{"repo": ".", "date": "2025-04-01", "line": "feat: Adjusted form width for mobile responsiveness", "commit": "27c0abea3147e5e00a5196fa9b351141a9d17ae2"}
+{"repo": ".", "date": "2025-04-02", "line": "fix: Handle missing last message gracefully", "commit": "b365afc910522a484ad257af6df7b607712c71f2"}
+{"repo": ".", "date": "2025-04-02", "line": "Error code: 422 - {'error': {'message': 'Provider returned error', 'code': 422, 'metadata': {'raw': '{\"detail\":[{\"type\":\"model_attributes_type\",\"loc\":[\"body\",\"response_format\"],\"msg\":\"Input should be a valid dictionary or object to extract fields from\",\"input\":\"json\"}]}', 'provider_name': 'DeepInfra'}}, 'user_id': 'user_2wGl2Zqx0Xrkj5eQdwZSgLEFytg'}", "commit": "99b2beeab0d55242537b7ec810bfdd69feb47103"}
+{"repo": ".", "date": "2025-04-02", "line": "style: Adjusted layout and overflow properties for improved responsiveness.", "commit": "81479e7058feda9954fd74810d1294fb92e7a1c4"}
+{"repo": ".", "date": "2025-04-03", "line": "feat: Refactor settings view and sidebar", "commit": "d10768403d221fbd1d50a520c083b8d1be1b3a19"}
+{"repo": ".", "date": "2025-04-03", "line": "feat: Added profile settings page with nickname and description editing", "commit": "69482207461eec9c3c64ec297231989aa248dd9a"}
+{"repo": ".", "date": "2025-04-06", "line": "fix: Update icons in manifest for stability on Firefox Android", "commit": "c2b8061ac292f18949d81c660c5a314cb42bcc6e"}
+{"repo": ".", "date": "2025-04-07", "line": "fix: Resolved web manifest icon instability on Firefox Android", "commit": "75593fd6bb45ee6020e54f3e0de9b1ff0e6d4f5d"}
+{"repo": ".", "date": "2025-04-08", "line": "feat: Added IPython dependency and improved asyncio handling", "commit": "d71d5da6bcf22d2daf5ec59832f15fe02472b95c"}
+{"repo": ".", "date": "2025-04-08", "line": "feat: Introduce UserPropertyService for managing user properties", "commit": "d2e2bb811707b02f05cbf22d10ef1916b021c90d"}
+{"repo": ".", "date": "2025-04-08", "line": "feat: Added debug middleware and improved routing in WebdavApplication", "commit": "d23ed3711a464f1d796ed35e58dbcaf1db6b7d84"}
+{"repo": ".", "date": "2025-04-08", "line": "feat: Add debug middleware and improve WebDAV functionality\n\nThis commit introduces a debug middleware for request logging and enhances WebDAV functionality with caching and improved error handling. It also includes fixes for lock management and directory size calculations.", "commit": "13f1d2f390afdfc912d24bb63930c9ca47e05f94"}
+{"repo": ".", "date": "2025-04-09", "line": "feat: Support short and standard YouTube links in template", "commit": "b31c286a8b8442d48f9b0713a8cce41432c168d1"}
+{"repo": ".", "date": "2025-04-09", "line": "feat: Add YouTube video embedding functionality", "commit": "b0a97ad267b971f8ba298bb5a0e696810c08b026"}
+{"repo": ".", "date": "2025-04-09", "line": "feat: Implement YouTube video embedding with improved parsing", "commit": "c6575d8e525dfa2f574e1965cd3df4379cde7acd"}
+{"repo": ".", "date": "2025-04-09", "line": "feat: Simplify YouTube video embedding logic", "commit": "6138cad7827c48a86b20d4015dce818dca348f04"}
+{"repo": ".", "date": "2025-04-09", "line": "feat: Style YouTube embed to center on page", "commit": "087f9c10b44fca9f29de862562b405ef5586f151"}
+{"repo": ".", "date": "2025-04-09", "line": "feat: Add support for YouTube video embedding", "commit": "e6bd7aa15211ae0bd3be65be3a659526b1131eee"}
+{"repo": ".", "date": "2025-04-09", "line": "feat: Added video embedding functionality with improved layout", "commit": "94e94cf7ca4bdcdd581dfe074728e93412c2a621"}
+{"repo": ".", "date": "2025-04-09", "line": "feat: Implement YouTube video embedding", "commit": "6673f7b615508f0c344fe0efbebe362f5236bd84"}
+{"repo": ".", "date": "2025-04-09", "line": "feat: Implement YouTube video embedding from text", "commit": "2582df360ab0667a3d29c46b92ad4abeb397d363"}
+{"repo": ".", "date": "2025-04-09", "line": "feat: Add video embedding functionality", "commit": "656ea5f90ee56a16b0f0047cace848572dc479c7"}
+{"repo": ".", "date": "2025-04-09", "line": "feat: Simplify YouTube embedding logic", "commit": "c529fc87fd6ed7b39bf057bce44ef30d1bc17f1b"}
+{"repo": ".", "date": "2025-04-09", "line": "feat: Implement YouTube video embedding", "commit": "8fa216c06cfaf3cd249e6c44efb5e5b2735f8c6a"}
+{"repo": ".", "date": "2025-04-09", "line": "feat: Refactor asyncio and database preparation in Application class\n\nfix: Corrected indentation and removed unnecessary comments in profile form\n\nrefactor: Improve user property service for JSON handling and querying\n\nstyle: Minor formatting adjustments in template embedding\n\nfix: Corrected profile view logic for user data retrieval and form handling\n\nrefactor: Improved webdav application and file handling", "commit": "44dd77cec5639575cb86973eceb8d174d570370c"}
+{"repo": ".", "date": "2025-04-09", "line": "feat: Minor formatting adjustments across modules", "commit": "743593affe276ae8ffd3751c80fe88eb4c99ac7f"}
+{"repo": ".", "date": "2025-04-10", "line": "feat: Removed comments and added channel display", "commit": "0e6fbd523cd4f4279a4f230567504b30c9b3116d"}
+{"repo": ".", "date": "2025-04-10", "line": "feat: Improved channel broadcasting and added user UID retrieval\n\nThis commit enhances the channel broadcasting mechanism by retrieving user UIDs directly from the channel member service. It also introduces a new method `get_user_uids` in the `ChannelMemberService` for efficient UID retrieval. Error handling has been improved in `SocketService` and `RPCView`.", "commit": "3594ac1f5984953487e0c3423c9672b01e416c28"}
+{"repo": ".", "date": "2025-04-13", "line": "feat: Add stats view and cache statistics tracking", "commit": "bc65752ea252cdcd929ba0bd956455317958337a"}
+{"repo": ".", "date": "2025-04-13", "line": "feat: Added stats view endpoint", "commit": "a1840cd034e7a4c792e2bcc69ff06595b1e2add3"}
+{"repo": ".", "date": "2025-04-13", "line": "refactor: Moved 'r' executable and updated .bashrc for automatic updates", "commit": "22668f8a72994446ffaa109e5ae742bd61bd3bf2"}
+{"repo": ".", "date": "2025-04-13", "line": "feat: Added initial .rcontext.txt file with facts and work procedure", "commit": "ec9af49f2903682cea978db15422fba4624c488d"}
+{"repo": ".", "date": "2025-04-13", "line": "docs: Added a reminder to be rude but functional.", "commit": "9b49e659e575e99de717a5c64e1ba1c3c4039cb1"}
+{"repo": ".", "date": "2025-04-13", "line": "refactor: Improve process handling and error management in TerminalSession", "commit": "823892a3021e674fea933b717565518dc1696031"}
+{"repo": ".", "date": "2025-04-13", "line": "fix: Return empty list when search query is empty", "commit": "4a770848a6dbc558c029b083a881becf7adef8d7"}
+{"repo": ".", "date": "2025-04-13", "line": "fix: Require both logged_in and uid for login_required views", "commit": "e4b0625799d9efd89e7e9518278588158b296c6c"}
+{"repo": ".", "date": "2025-04-13", "line": "feat: Require login for search user view", "commit": "8ae9aac045e41fc84ebab102335a2613b3e22c08"}
+{"repo": ".", "date": "2025-04-13", "line": "feat: Allow profile description editing and nickname updates\n\nThis commit introduces profile editing functionality, including a textarea for the description and an input field for the nickname. It also fixes a bug in the user property service and adds a redirect after saving changes.", "commit": "bee7d828cd67581c33946630cd22fe8edd674d15"}
+{"repo": ".", "date": "2025-04-14", "line": "feat: Add user profile page and link avatars to user profiles", "commit": "3b05acffd296169eed305a55dba79d632d5f78f5"}
+{"repo": ".", "date": "2025-04-14", "line": "feat: Implement user profile view and template", "commit": "a3abd854bbf0ebe2ef0ef46e7c346a995e5b6faa"}
+{"repo": ".", "date": "2025-04-14", "line": "feat: Refactor user property setting logic using upsert", "commit": "9fb6e64655dff132be43e2fc867827d17ad94201"}
+{"repo": ".", "date": "2025-04-14", "line": "feat: Improve user profile rendering and link avatar", "commit": "0fa04883850534fbb97755e06ebec1538dccfdc7"}
+{"repo": ".", "date": "2025-04-14", "line": "fix: Use user uid instead of request user uid in user_property.set", "commit": "d4f5a4640929b1f16ccddc741f977cf7e901e7de"}
+{"repo": ".", "date": "2025-04-14", "line": "feat: Added back button to user page sidebar", "commit": "3cfb79c8f560430639fceb4a278fc81dfbad2299"}
+{"repo": ".", "date": "2025-04-14", "line": "fix: Removed fixed positioning from header on smaller screens", "commit": "c36ce17da5fcccfdaaf7ddf7579b0399519d078f"}
+{"repo": ".", "date": "2025-04-14", "line": "feat: Added chat area class to user profile section", "commit": "4cc70640e4f7c4d65e5a0c3a503aae1f891164d5"}
+{"repo": ".", "date": "2025-04-17", "line": "feat: Refactor layout and styling for user profile page", "commit": "1cd0b54656a969bcb87cbdc07687866ca43e650b"}
+{"repo": ".", "date": "2025-05-06", "line": "feat: Added command-line arguments and executable script", "commit": "46a8b612b49f1094c0a8520d97d4b5642f2a57e9"}
+{"repo": ".", "date": "2025-05-06", "line": "feat: Configure setuptools to find packages in src directory", "commit": "061da150f9779c6130fac0c957d4facdd59aa33a"}
+{"repo": ".", "date": "2025-05-06", "line": "feat: Handle missing pty on Windows", "commit": "6312dfae47b09753675b038798cf38f00311e772"}
+{"repo": ".", "date": "2025-05-06", "line": "feat: Added UUID generation and updated hashing functions", "commit": "c709ee11c99f54b58844165b6eb9993240ab0005"}
+{"repo": ".", "date": "2025-05-06", "line": "feat: Added paste and file drop support for message input", "commit": "f7fda2d2c951a07bccc927a333b7feaa527556c2"}
+{"repo": ".", "date": "2025-05-06", "line": "fix: Use user home folder for uploads", "commit": "529ebd23fc0b50e2606ecddc5e1199774dd18384"}
+{"repo": ".", "date": "2025-05-06", "line": "Merge feat/copy-paste-drag-drop", "commit": "0f6eb5c043325e0aa0c77a587672c1d3a5dcb9fd"}
+{"repo": ".", "date": "2025-05-06", "line": "feat: Implemented file paste and drag-and-drop support", "commit": "b0666a00900e1b25633433b80da1ef3dd5f2ee71"}
+{"repo": ".", "date": "2025-05-08", "line": "feat: Dispatch upload event and focus input after upload", "commit": "707788583a2387c1729950e08243d3f8f7049d7c"}
+{"repo": ".", "date": "2025-05-08", "line": "feat: Added focus functionality with Escape and G keybindings", "commit": "d6d2f2892ba3045e5555e9fb4b3d63adf51e2fc2"}
+{"repo": ".", "date": "2025-05-08", "line": "fix: Prevent double focus loss during upload escape key press", "commit": "fa59dbc095b65c7b43a1b7f0e70541bd1fd0302c"}
+{"repo": ".", "date": "2025-05-08", "line": "feat: Focus input field after message display", "commit": "e153811ff34ef63892cc6aac1d5afd92cb510d14"}
+{"repo": ".", "date": "2025-05-08", "line": "feat: Focus input field after message display", "commit": "0a3e15137761d333211d8b52d178f5e150a579f1"}
+{"repo": ".", "date": "2025-05-08", "line": "feat: Focus upload while shift+G is pressed", "commit": "f6706c165e2a8ba392bde1e81d8006381fed96d3"}
+{"repo": ".", "date": "2025-05-08", "line": "feat: Focus input field after layout update during upload", "commit": "8799662159656867494b2774073b3bfb1bbe5178"}
+{"repo": ".", "date": "2025-05-08", "line": "feat: Focus input field after upload", "commit": "49ec99ef016dc754e36442d774da1d3a712bf2a7"}
+{"repo": ".", "date": "2025-05-08", "line": "feat: Updated Dockerfiles and terminal configuration\n\nThis commit updates the Dockerfiles and terminal configuration with several improvements:\n\n-   Added `xterm`, `valgrind`, `ack`, `irssi`, and `lynx` to the Ubuntu Dockerfile.\n-   Added Rust toolchain installation to the Ubuntu Dockerfile.\n-   Modified the `r` executable placement in the Ubuntu Dockerfile.\n-   Updated the terminal.html template to include fit addon and clear the screen on connection.\n-   Added `$HOME/bin` to the PATH in the .bashrc file.\n-   Removed the `.rcontext.txt` file.\n", "commit": "3c1d5d601fa1a9f30b2aa4fd36086102108dde94"}
+{"repo": ".", "date": "2025-05-08", "line": "feat: Updated Makefile with venv and ubuntu build target", "commit": "d0dd342e27cf4160f96faf87deff81f728e41e47"}
+{"repo": ".", "date": "2025-05-08", "line": "chore: Updated Makefile and .bashrc", "commit": "31062fddbfbbf25f206060b38773a7e2c008723c"}
+{"repo": ".", "date": "2025-05-08", "line": "feat: Install tmux", "commit": "3c0fea6812a5f5d759c08e6de984c0ccf5f9b9a9"}
+{"repo": ".", "date": "2025-05-09", "line": "feat: Implement user template loading based on admin UID", "commit": "02a0253c1d9c73d2918fecb8f52c4c7739c867f5"}
+{"repo": ".", "date": "2025-05-09", "line": "fix: Prevent appending None to template paths", "commit": "165dda32100e52c347c7f6bb71062244b2a50ba1"}
+{"repo": ".", "date": "2025-05-09", "line": "feat: Refactor static file serving with user-specific paths", "commit": "ac570d036c26b6c7ff5abac169fdf622c10827ad"}
+{"repo": ".", "date": "2025-05-09", "line": "feat: Pass self to static_handler", "commit": "b867b6ba78574a332bf951eb6c00a6a88ded325d"}
+{"repo": ".", "date": "2025-05-09", "line": "fix: Use request.session instead of self.request.session", "commit": "e359a8ebe294e0f55cf4164926011e893468e4bc"}
+{"repo": ".", "date": "2025-05-09", "line": "fix: Use user static path for templates", "commit": "5b28044d9e604b2404bf4b7277a240f7fc56032c"}
+{"repo": ".", "date": "2025-05-09", "line": "feat: Refactor static file serving with `add_static`", "commit": "c56bf4fb49c986e9b653f635a81937a3ef433a5e"}
+{"repo": ".", "date": "2025-05-09", "line": "feat: Added repository management functionality\n\nThis commit introduces repository management features, including creation, updating, and deletion. It includes new models, services, and views for handling repositories, along with updated templates for a user-friendly interface.  Also added uvloop for aiohttp.", "commit": "ee40c905d4448f0b39d28c4d51343b5fe111d038"}
+{"repo": ".", "date": "2025-05-09", "line": "feat: Added basic Git repository management functionality", "commit": "a5aac9a33701e3d4852fba13520771e6de82aac0"}
+{"repo": ".", "date": "2025-05-09", "line": "fix: Corrected repository deletion URL and implemented repository deletion functionality", "commit": "e06776d81d0fa40e4d9d5f57a6259df8db271372"}
+{"repo": ".", "date": "2025-05-09", "line": "feat: Add uvloop dependency and fix repository path resolution", "commit": "adb59eff68e5c855fbce6f930db1ea13f59683f6"}
+{"repo": ".", "date": "2025-05-09", "line": "feat: Implement basic authentication for git receive-pack endpoint", "commit": "3ae30f1f7645203a8e8c15bd298d802fffbd2334"}
+{"repo": ".", "date": "2025-05-09", "line": "feat: Increase client max size for uploads", "commit": "e5d155e1249f9df7c504a95c98171f7e4fe5d5a4"}
+{"repo": ".", "date": "2025-05-09", "line": "refactor: Migrate from argparse to click and improve application startup", "commit": "7e8ae1632d19238954ca96657da1d3950ebd413c"}
+{"repo": ".", "date": "2025-05-09", "line": "feat: Added repository view and related functionality", "commit": "95ad49df432195cb127f9fe695eac14678422b37"}
+{"repo": ".", "date": "2025-05-09", "line": "Error code: 422 - {'error': {'message': 'Provider returned error', 'code': 422, 'metadata': {'raw': '{\"detail\":[{\"type\":\"model_attributes_type\",\"loc\":[\"body\",\"response_format\"],\"msg\":\"Input should be a valid dictionary or object to extract fields from\",\"input\":\"json\"}]}', 'provider_name': 'DeepInfra'}}, 'user_id': 'user_2wGl2Zqx0Xrkj5eQdwZSgLEFytg'}", "commit": "17c6124a57a394c63427a0038e598fdb40560f15"}
+{"repo": ".", "date": "2025-05-09", "line": "feat: Initial file manager UI and basic repository view", "commit": "4c34d7eda58530eddb2c8b3479627180d6eeb248"}
+{"repo": ".", "date": "2025-05-09", "line": "Error code: 422 - {'error': {'message': 'Provider returned error', 'code': 422, 'metadata': {'raw': '{\"detail\":[{\"type\":\"model_attributes_type\",\"loc\":[\"body\",\"response_format\"],\"msg\":\"Input should be a valid dictionary or object to extract fields from\",\"input\":\"json\"}]}', 'provider_name': 'DeepInfra'}}, 'user_id': 'user_2wGl2Zqx0Xrkj5eQdwZSgLEFytg'}", "commit": "1616e4edb97284f705400c0598306202a083f60f"}
+{"repo": ".", "date": "2025-05-09", "line": "feat: Update dependencies and refactor repository view for improved navigation and file handling", "commit": "44ac1d2bfaa32b99d6ee51d65efdc170d846b1f8"}
+{"repo": ".", "date": "2025-05-09", "line": "feat: Added DBService and RPCView db methods", "commit": "dd108c20044540c3801ac461c612392bed76ff89"}
+{"repo": ".", "date": "2025-05-10", "line": "feat: Added Drive API and HTML views for file management", "commit": "f0591d493955d9c126e7dee6d1a06917c48bbbd2"}
+{"repo": ".", "date": "2025-05-10", "line": "feat: Implemented typing indicator and glow effect for active users", "commit": "3412aa0bf0c0bb138c234f88fad55cb69267df79"}
+{"repo": ".", "date": "2025-05-10", "line": "feat: Added channel attachment functionality with file uploads and views", "commit": "9133b7c3ce6457fa6c218b540828c752b4ba5c72"}
+{"repo": ".", "date": "2025-05-10", "line": "fix: Remove debug print statement in SocketService", "commit": "4d7566de9bb3f2c54954fe72d0332caecd133ffa"}
+{"repo": ".", "date": "2025-05-10", "line": "refactor: Remove hardcoded repo and user configurations", "commit": "2c9004418555dfc2a4c826e5c30aa0d59f332df7"}
+{"repo": ".", "date": "2025-05-11", "line": "feat: Added SSH server functionality with user-specific home directories and authentication.", "commit": "01846bf23f7883007b99a2e100240bf3b35b30f2"}
+{"repo": ".", "date": "2025-05-11", "line": "fix: Updated SSH port to 2242", "commit": "c48b84bf3ab7cff5dee5670e23db3d771e14fc46"}
+{"repo": ".", "date": "2025-05-12", "line": "feat: Add image conversion and resizing support in channel attachments", "commit": "f156a153de1b2f89b99cf0490eb18bf27a611fe1"}
+{"repo": ".", "date": "2025-05-13", "line": "feat: Added image conversion and resizing support for channel attachments", "commit": "ac2f68f93fd66c0d6ab3682525b2d9c94febff4d"}
+{"repo": ".", "date": "2025-05-13", "line": "feat: Improve Windows compatibility and database initialization.", "commit": "a4bea9449526fc8f6b01c02d777db5c30186b830"}
+{"repo": ".", "date": "2025-05-13", "line": "feat: Added WebSocket synchronization and testing scripts", "commit": "ba3152f553afcfae318811a413cdea6f5be9f413"}
+{"repo": ".", "date": "2025-05-13", "line": "refactor: Moved WebSocketClient to system/websocket.py", "commit": "adad5ed4fe37038442d247d9246795a82d31093c"}
+{"repo": ".", "date": "2025-05-13", "line": "fix: Remove timing output from task execution", "commit": "2e324ff11815d3c67fffa8e8d5f3e3554f154b57"}
+{"repo": ".", "date": "2025-05-13", "line": "feat: Implement image click to view full size", "commit": "d09055986e9a5d971f58075a5e939a268deb26be"}
+{"repo": ".", "date": "2025-05-13", "line": "fix: Add width parameter to image URLs", "commit": "8cd2f16c5c46318cb035b882197b516ae5532452"}
+{"repo": ".", "date": "2025-05-13", "line": "feat: Added width attribute to image source", "commit": "015b188d5ea16a75d4ae6ef0d9bd6c2514e68fda"}
+{"repo": ".", "date": "2025-05-13", "line": "fix: Corrected image width attribute in template rendering", "commit": "12d287042415554c581e7d4fcfc81bd3d733fa02"}
+{"repo": ".", "date": "2025-05-13", "line": "feat: Add width parameter to image source", "commit": "964a747f42ade75dcfced5395f46727f0508172c"}
+{"repo": ".", "date": "2025-05-13", "line": "feat: Add width parameter to image sources", "commit": "319c1b1b5264933a7ea1d7af6541be2a410a3328"}
+{"repo": ".", "date": "2025-05-13", "line": "fix: Corrected webp image type in template", "commit": "a21e3590ef4ddad292fb914cb3454d07eb622413"}
+{"repo": ".", "date": "2025-05-13", "line": "fix: Reduce image width and fix image URL in web template", "commit": "b55d74fb124b90ee24d158bc94c401b0ff19edb9"}
+{"repo": ".", "date": "2025-05-13", "line": "style: Added cursor pointer to chat message images", "commit": "3858dcbd62e4032a02e9d25dffd000ade4dc7bbe"}
+{"repo": ".", "date": "2025-05-13", "line": "feat: Add height parameter to image rendering", "commit": "0ea0cd96dbe536b09cb3549de47766a756f04008"}
+{"repo": ".", "date": "2025-05-13", "line": "feat: Encode attachment URLs for safe transmission", "commit": "af1cf4f5aee9cc07c1c8a8e8039c19001e3d6ea9"}
+{"repo": ".", "date": "2025-05-13", "line": "fix: Corrected attachment URL and added scroll to drive view", "commit": "c45b61681dc2fc2a239e1bc2672da44f7738b0c6"}
+{"repo": ".", "date": "2025-05-15", "line": "feat: Introduce online user list and typing indicator", "commit": "db6d6c0106267f56822ae378a4c88385d025051a"}
+{"repo": ".", "date": "2025-05-15", "line": "feat: Updated message text updating and added message age check.", "commit": "25d109beedf030523ac5d357dbbb1f9efb919edb"}
+{"repo": ".", "date": "2025-05-15", "line": "feat: Implemented dialogs for online users and help, and updated templates", "commit": "dd80f3732b7f500acdd92f6e44f42f9ade0f205b"}
+{"repo": ".", "date": "2025-05-15", "line": "feat: Added /live command to help dialog", "commit": "79c39828f0a3282f53e3322e19b211b5559466a1"}
+{"repo": ".", "date": "2025-05-16", "line": "feat: Implement user availability service and update logic", "commit": "c5b55399a1fbea233b33a9e5fdde1fe2cd9167aa"}
+{"repo": ".", "date": "2025-05-16", "line": "feat: Update socket service and attachment view\n\n- Added last_ping to user data on socket connection.\n- Fixed bug in attachment view where format was not correctly passed to image.save.\n- Improved attachment view to handle multiple attachments.", "commit": "93462d4c4b93c4f7eb81702801df9159cbb64e8e"}
+{"repo": ".", "date": "2025-05-16", "line": "fix: Increased last ping cooldown to 180 seconds", "commit": "c387225a6e8aa826b944ff3c53c4045db25db758"}
+{"repo": ".", "date": "2025-05-16", "line": "feat: Added cache enable/disable functionality", "commit": "00557ec9eaab7256d5c129fc8c00c12650ea3fd3"}
+{"repo": ".", "date": "2025-05-17", "line": "feat: Refactor index.html with improved styling and content", "commit": "c0b4ba715c329273e4f5684d1ec2e231e5a1c7e7"}
+{"repo": ".", "date": "2025-05-17", "line": "feat: Refactor chat input component with improved auto-completion and live typing functionality", "commit": "48c3daf3983e3b6e04a0c5888febceb69db9d661"}
+{"repo": ".", "date": "2025-05-17", "line": "feat: Added star animation to sandbox page", "commit": "e79abf4a26454cddf766cd1ba138554817c820cb"}
diff --git a/src/snek/templates/sandbox.html b/src/snek/templates/sandbox.html
index 6c89c98..f77101a 100644
--- a/src/snek/templates/sandbox.html
+++ b/src/snek/templates/sandbox.html
@@ -5,11 +5,86 @@ import { app } from "/app.js";
 const STAR_COUNT = 200;
 const body = document.body;
 
+function getStarPosition(star) {
+    const leftPercent = parseFloat(star.style.left);
+    const topPercent = parseFloat(star.style.top);
+
+    let position;
+
+    if (topPercent < 40 && leftPercent >= 40 && leftPercent <= 60) {
+      position = 'North';
+    } else if (topPercent > 60 && leftPercent >= 40 && leftPercent <= 60) {
+      position = 'South';
+    } else if (leftPercent < 40 && topPercent >= 40 && topPercent <= 60) {
+      position = 'West';
+    } else if (leftPercent > 60 && topPercent >= 40 && topPercent <= 60) {
+      position = 'East';
+    } else if (topPercent >= 40 && topPercent <= 60 && leftPercent >= 40 && leftPercent <= 60) {
+      position = 'Center';
+    } else {
+      position = 'Corner or Edge';
+    }
+    return position 
+}
+let stars = {}
+window.stars = stars
+
+
 function createStar() {
   const star = document.createElement('div');
   star.classList.add('star');
   star.style.left = `${Math.random() * 100}%`;
   star.style.top = `${Math.random() * 100}%`;
+  star.shuffle = () => {
+    star.style.left = `${Math.random() * 100}%`;
+    star.style.top = `${Math.random() * 100}%`;
+ 
+  star.position = getStarPosition(star)
+ }
+  star.position = getStarPosition(star)
+
+function moveStarToPosition(star, position) {
+  let top, left;
+
+  switch (position) {
+    case 'North':
+      top = `${Math.random() * 20}%`;
+      left = `${40 + Math.random() * 20}%`;
+      break;
+    case 'South':
+      top = `${80 + Math.random() * 10}%`;
+      left = `${40 + Math.random() * 20}%`;
+      break;
+    case 'West':
+      top = `${40 + Math.random() * 20}%`;
+      left = `${Math.random() * 20}%`;
+      break;
+    case 'East':
+      top = `${40 + Math.random() * 20}%`;
+      left = `${80 + Math.random() * 10}%`;
+      break;
+    case 'Center':
+      top = `${45 + Math.random() * 10}%`;
+      left = `${45 + Math.random() * 10}%`;
+      break;
+    default: // 'Corner or Edge' fallback
+      top = `${Math.random() * 100}%`;
+      left = `${Math.random() * 100}%`;
+      break;
+  }
+
+  star.style.top = top;
+  star.style.left = left;
+
+  star.position = getStarPosition(star)
+}
+
+
+
+
+  if(!stars[star.position])
+      stars[star.position] = []
+  stars[star.position].push(star)
   const size = Math.random() * 2 + 1;
   star.style.width = `${size}px`;
   star.style.height = `${size}px`;
@@ -53,6 +128,320 @@ app.updateStarColor = updateStarColorDelayed;
 app.ws.addEventListener("set_typing", (data) => {
   updateStarColorDelayed(data.data.color);
 });
+window.createAvatar = () => {
+    let avatar = document.createElement("avatar-face")
+    document.querySelector("main").appendChild(avatar)
+    return avatar
+}
+
+  
+    class AvatarFace extends HTMLElement {
+      static get observedAttributes(){
+        return ['emotion','face-color','eye-color','text','balloon-color','text-color'];
+      }
+      constructor(){
+        super();
+        this._shadow = this.attachShadow({mode:'open'});
+        this._shadow.innerHTML = `
+          <style>
+            :host { display:block; position:relative; }
+            canvas { width:100%; height:100%; display:block; }
+          </style>
+          <canvas></canvas>
+        `;
+        this._c   = this._shadow.querySelector('canvas');
+        this._ctx = this._c.getContext('2d');
+
+        // state
+        this._mouse      = {x:0,y:0};
+        this._blinkTimer = 0;
+        this._blinking   = false;
+        this._lastTime   = 0;
+
+        // defaults
+        this._emotion      = 'neutral';
+        this._faceColor    = '#ffdfba';
+        this._eyeColor     = '#000';
+        this._text         = '';
+        this._balloonColor = '#fff';
+        this._textColor    = '#000';
+      }
+
+      attributeChangedCallback(name,_old,newV){
+        if (name==='emotion')       this._emotion      = newV||'neutral';
+        else if (name==='face-color')   this._faceColor    = newV||'#ffdfba';
+        else if (name==='eye-color')    this._eyeColor     = newV||'#000';
+        else if (name==='text')         this._text         = newV||'';
+        else if (name==='balloon-color')this._balloonColor = newV||'#fff';
+        else if (name==='text-color')   this._textColor    = newV||'#000';
+      }
+
+      connectedCallback(){
+        // watch size so canvas buffer matches display
+        this._ro = new ResizeObserver(entries=>{
+          for(const ent of entries){
+            const w = ent.contentRect.width;
+            const h = ent.contentRect.height;
+            const dpr = devicePixelRatio||1;
+            this._c.width  = w*dpr;
+            this._c.height = h*dpr;
+            this._ctx.scale(dpr,dpr);
+          }
+        });
+        this._ro.observe(this);
+
+        // track mouse so eyes follow
+        this._shadow.addEventListener('mousemove', e=>{
+          const r = this._c.getBoundingClientRect();
+          this._mouse.x = e.clientX - r.left;
+          this._mouse.y = e.clientY - r.top;
+        });
+
+        this._lastTime = performance.now();
+        this._raf      = requestAnimationFrame(t=>this._loop(t));
+      }
+
+      disconnectedCallback(){
+        cancelAnimationFrame(this._raf);
+        this._ro.disconnect();
+      }
+
+      _updateBlink(dt){
+        this._blinkTimer -= dt;
+        if (this._blinkTimer<=0){
+          this._blinking = !this._blinking;
+          this._blinkTimer = this._blinking
+            ? 0.1
+            : 2 + Math.random()*3;
+        }
+      }
+
+      _roundRect(x,y,w,h,r){
+        const ctx = this._ctx;
+        ctx.beginPath();
+        ctx.moveTo(x+r,y);
+        ctx.lineTo(x+w-r,y);
+        ctx.quadraticCurveTo(x+w,y, x+w,y+r);
+        ctx.lineTo(x+w,y+h-r);
+        ctx.quadraticCurveTo(x+w,y+h, x+w-r,y+h);
+        ctx.lineTo(x+r,y+h);
+        ctx.quadraticCurveTo(x,y+h, x,y+h-r);
+        ctx.lineTo(x,y+r);
+        ctx.quadraticCurveTo(x,y, x+r,y);
+        ctx.closePath();
+      }
+
+      _draw(ts){
+        const ctx = this._ctx;
+        const W = this._c.clientWidth;
+        const H = this._c.clientHeight;
+        ctx.clearRect(0,0,W,H);
+
+        // HEAD + BOB
+        const cx = W/2;
+        const cy = H/2 + Math.sin(ts*0.002)*8;
+        const R  = Math.min(W,H)*0.25;
+
+        // SPEECH BALLOON
+        if (this._text){
+          const pad = 6;
+          ctx.font = `${R*0.15}px sans-serif`;
+          const m = ctx.measureText(this._text);
+          const tw = m.width, th = R*0.18;
+          const bw = tw + pad*2, bh = th + pad*2;
+          const bx = cx - bw/2, by = cy - R - bh - 10;
+          // bubble
+          ctx.fillStyle = this._balloonColor;
+          this._roundRect(bx,by,bw,bh,6);
+          ctx.fill();
+          ctx.strokeStyle = '#888';
+          ctx.lineWidth = 1.2;
+          ctx.stroke();
+          // tail
+          ctx.beginPath();
+          ctx.moveTo(cx-6, by+bh);
+          ctx.lineTo(cx+6, by+bh);
+          ctx.lineTo(cx, cy-R+4);
+          ctx.closePath();
+          ctx.fill();
+          ctx.stroke();
+          // text
+          ctx.fillStyle = this._textColor;
+          ctx.textBaseline = 'top';
+          ctx.fillText(this._text, bx+pad, by+pad);
+        }
+
+        // FACE
+        ctx.fillStyle = this._faceColor;
+        ctx.beginPath();
+        ctx.arc(cx,cy,R,0,2*Math.PI);
+        ctx.fill();
+
+        // EYES
+        const eyeY = cy - R*0.2;
+        const eyeX = R*0.4;
+        const eyeR= R*0.12;
+        const pupR= eyeR*0.5;
+
+        for(let i=0;i<2;i++){
+          const ex = cx + (i? eyeX:-eyeX);
+          const ey = eyeY;
+          // eyeball
+          ctx.fillStyle = '#fff';
+          ctx.beginPath();
+          ctx.arc(ex,ey,eyeR,0,2*Math.PI);
+          ctx.fill();
+          // pupil follows
+          let dx = this._mouse.x - ex;
+          let dy = this._mouse.y - ey;
+          const d = Math.hypot(dx,dy);
+          const max = eyeR - pupR - 2;
+          if (d>max){ dx=dx/d*max; dy=dy/d*max; }
+          if (this._blinking){
+            ctx.strokeStyle='#000';
+            ctx.lineWidth=3;
+            ctx.beginPath();
+            ctx.moveTo(ex-eyeR,ey);
+            ctx.lineTo(ex+eyeR,ey);
+            ctx.stroke();
+          } else {
+            ctx.fillStyle = this._eyeColor;
+            ctx.beginPath();
+            ctx.arc(ex+dx,ey+dy,pupR,0,2*Math.PI);
+            ctx.fill();
+          }
+        }
+
+        // ANGRY BROWS
+        if (this._emotion==='angry'){
+          ctx.strokeStyle='#000';
+          ctx.lineWidth=4;
+          [[-eyeX,1],[ eyeX,-1]].forEach(([off,dir])=>{
+            const sx = cx+off - eyeR;
+            const sy = eyeY - eyeR*1.3;
+            const ex = cx+off + eyeR;
+            const ey2= sy + dir*6;
+            ctx.beginPath();
+            ctx.moveTo(sx,sy);
+            ctx.lineTo(ex,ey2);
+            ctx.stroke();
+          });
+        }
+
+        // MOUTH by emotion
+        const mw = R*0.6;
+        const my = cy + R*0.25;
+        ctx.strokeStyle='#a33';
+        ctx.lineWidth=4;
+
+        if (this._emotion==='surprised'){
+          ctx.fillStyle='#a33';
+          ctx.beginPath();
+          ctx.arc(cx,my,mw*0.3,0,2*Math.PI);
+          ctx.fill();
+        }
+        else if (this._emotion==='sad'){
+          ctx.beginPath();
+          ctx.arc(cx,my,mw/2,1.15*Math.PI,1.85*Math.PI,true);
+          ctx.stroke();
+        }
+        else if (this._emotion==='angry'){
+          ctx.beginPath();
+          ctx.moveTo(cx-mw/2,my+2);
+          ctx.lineTo(cx+mw/2,my-2);
+          ctx.stroke();
+        }
+        else {
+          const s = this._emotion==='happy'? 0.15*Math.PI:0.2*Math.PI;
+          const e = this._emotion==='happy'? 0.85*Math.PI:0.8*Math.PI;
+          ctx.beginPath();
+          ctx.arc(cx,my,mw/2,s,e);
+          ctx.stroke();
+        }
+      }
+
+      _loop(ts){
+        const dt = (ts - this._lastTime)/1000;
+        this._lastTime = ts;
+        this._updateBlink(dt);
+        this._draw(ts);
+        this._raf = requestAnimationFrame(t=>this._loop(t));
+      }
+    }
+    customElements.define('avatar-face', AvatarFace);
+
+
+  class AvatarReplacer {
+    constructor(target, opts={}){
+      this.target = target;
+      // record original inline styles so we can restore
+      this._oldVis = target.style.visibility || '';
+      this._oldPos = target.style.position || '';
+      // hide the target
+      target.style.visibility = 'hidden';
+      // measure
+      const rect = target.getBoundingClientRect();
+      // create avatar
+      this.avatar = document.createElement('avatar-face');
+      // copy all supported opts into attributes
+      ['emotion','faceColor','eyeColor','text','balloonColor','textColor']
+        .forEach(k => {
+          const attr = k.replace(/[A-Z]/g,m=>'-'+m.toLowerCase());
+          if (opts[k] != null) this.avatar.setAttribute(attr, opts[k]);
+        });
+      // position absolutely
+      const scrollX = window.pageXOffset;
+      const scrollY = window.pageYOffset;
+      Object.assign(this.avatar.style, {
+        position: 'absolute',
+        left: (rect.left + scrollX) + 'px',
+        top:  (rect.top  + scrollY) + 'px',
+        width:  rect.width  + 'px',
+        height: rect.height + 'px',
+        zIndex: 9999
+      });
+      document.body.appendChild(this.avatar);
+    }
+
+    detach(){
+      // remove avatar and restore target
+      if (this.avatar && this.avatar.parentNode) {
+        this.avatar.parentNode.removeChild(this.avatar);
+        this.avatar = null;
+      }
+      this.target.style.visibility = this._oldVis;
+      this.target.style.position   = this._oldPos;
+    }
+
+    // static convenience method
+    static attach(target, opts){
+      return new AvatarReplacer(target, opts);
+    }
+  }
+/*
+  // DEMO wiring
+  const btnGo = document.getElementById('go');
+  const btnReset = document.getElementById('reset');
+  let repl1, repl2;
+
+  btnGo.addEventListener('click', ()=>{
+    // replace #one with a happy avatar saying "Hi!"
+    repl1 = AvatarReplacer.attach(
+      document.getElementById('one'),
+      {emotion:'happy', text:'Hi!', balloonColor:'#fffbdd', textColor:'#333'}
+    );
+    // replace #two with a surprised avatar
+    repl2 = AvatarReplacer.attach(
+      document.getElementById('two'),
+      {emotion:'surprised', faceColor:'#eeffcc', text:'Wow!', balloonColor:'#ddffdd'}
+    );
+  });
+
+  btnReset.addEventListener('click', ()=>{
+    if (repl1) repl1.detach();
+    if (repl2) repl2.detach();
+  });
+*/
 
 /*
 class StarField {
diff --git a/src/snek/view/avatar.py b/src/snek/view/avatar.py
index a85b876..ad81301 100644
--- a/src/snek/view/avatar.py
+++ b/src/snek/view/avatar.py
@@ -29,12 +29,35 @@ from aiohttp import web
 from multiavatar import multiavatar
 
 from snek.system.view import BaseView
+from snek.view.avatar_animal import generate_avatar_with_options
 
+import functools
 
 class AvatarView(BaseView):
     login_required = False
+    
+    def __init__(self, *args,**kwargs):
+        super().__init__(*args,**kwargs)
+        self.avatars = {}
 
     async def get(self):
+        uid = self.request.match_info.get("uid")
+        while True:
+            try:
+                return web.Response(text=self._get(uid), content_type="image/svg+xml")
+            except Exception as e:
+                pass
+
+    
+    def _get(self, uid):
+        if uid in self.avatars:
+            return self.avatars[uid]
+
+        avatar = generate_avatar_with_options(self.request.query)
+        self.avatars[uid] = avatar
+        return avatar
+
+    async def get2(self):
         uid = self.request.match_info.get("uid")
         if uid == "unique":
             uid = str(uuid.uuid4())
diff --git a/src/snek/view/avatar_animal.py b/src/snek/view/avatar_animal.py
new file mode 100644
index 0000000..3a7f653
--- /dev/null
+++ b/src/snek/view/avatar_animal.py
@@ -0,0 +1,871 @@
+import random
+import math
+import argparse
+import json
+from typing import Dict, List, Tuple, Optional, Union
+
+class AnimalAvatarGenerator:
+    """A generator for animal-themed avatar SVGs."""
+    
+    # Constants
+    ANIMALS = [
+        "cat", "dog", "fox", "wolf", "bear", "panda", "koala", "tiger", "lion", 
+        "rabbit", "monkey", "elephant", "giraffe", "zebra", "penguin", "owl",
+        "deer", "raccoon", "squirrel", "hedgehog", "otter", "frog"
+    ]
+    
+    COLOR_PALETTES = {
+        "natural": {
+            "cat": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000", "#808080", "#FFFFFF", "#FFA500"],
+            "dog": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000", "#808080", "#FFFFFF", "#FFA500"],
+            "fox": ["#FF6600", "#FF7F00", "#FF8C00", "#FFA500", "#FFFFFF", "#000000"],
+            "wolf": ["#808080", "#A9A9A9", "#D3D3D3", "#FFFFFF", "#000000", "#696969"],
+            "bear": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000", "#FFFFFF"],
+            "panda": ["#000000", "#FFFFFF"],
+            "koala": ["#808080", "#A9A9A9", "#D3D3D3", "#FFFFFF", "#000000"],
+            "tiger": ["#FF8C00", "#FF7F00", "#FFFFFF", "#000000"],
+            "lion": ["#DAA520", "#B8860B", "#CD853F", "#D2B48C", "#FFFFFF", "#000000"],
+            "rabbit": ["#FFFFFF", "#F5F5F5", "#D3D3D3", "#A9A9A9", "#FFC0CB", "#000000"],
+            "monkey": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000"],
+            "elephant": ["#808080", "#A9A9A9", "#D3D3D3", "#FFFFFF", "#000000"],
+            "giraffe": ["#DAA520", "#B8860B", "#F5DEB3", "#FFFFFF", "#000000"],
+            "zebra": ["#000000", "#FFFFFF"],
+            "penguin": ["#000000", "#FFFFFF", "#FFA500"],
+            "owl": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000", "#FFFFFF", "#FFC0CB"],
+            "deer": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#FFFFFF", "#000000"],
+            "raccoon": ["#808080", "#A9A9A9", "#D3D3D3", "#FFFFFF", "#000000"],
+            "squirrel": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000"],
+            "hedgehog": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000"],
+            "otter": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000"],
+            "frog": ["#008000", "#00FF00", "#ADFF2F", "#7FFF00", "#000000", "#FFFFFF"]
+        },
+        "pastel": {
+            "all": ["#FFB6C1", "#FFD700", "#FFDAB9", "#98FB98", "#ADD8E6", "#DDA0DD", "#F0E68C", "#FFFFE0"]
+        },
+        "vibrant": {
+            "all": ["#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", "#FFA500", "#FF1493"]
+        },
+        "mono": {
+            "all": ["#000000", "#333333", "#666666", "#999999", "#CCCCCC", "#FFFFFF"]
+        }
+    }
+    
+    EYE_STYLES = ["round", "oval", "almond", "wide", "narrow", "cute"]
+    
+    FACE_SHAPES = ["round", "oval", "square", "heart", "triangular", "diamond"]
+    
+    EAR_STYLES = {
+        "cat": ["pointed", "folded", "round", "large", "small"],
+        "dog": ["floppy", "pointed", "round", "large", "small"],
+        "fox": ["pointed", "large", "small"],
+        "wolf": ["pointed", "large", "small"],
+        "bear": ["round", "small"],
+        "panda": ["round", "small"],
+        "koala": ["round", "large"],
+        "tiger": ["round", "small"],
+        "lion": ["round", "small"],
+        "rabbit": ["long", "floppy", "standing"],
+        "monkey": ["round", "small"],
+        "elephant": ["large", "wide"],
+        "giraffe": ["small", "pointed"],
+        "zebra": ["pointed", "small"],
+        "penguin": ["none"],
+        "owl": ["none", "tufted"],
+        "deer": ["small", "pointed"],
+        "raccoon": ["round", "small"],
+        "squirrel": ["pointed", "small"],
+        "hedgehog": ["round", "small"],
+        "otter": ["round", "small"],
+        "frog": ["none"]
+    }
+    
+    NOSE_STYLES = ["round", "triangular", "small", "large", "heart", "button"]
+    
+    SPECIAL_FEATURES = {
+        "cat": ["whiskers", "stripes", "spots"],
+        "dog": ["spots", "patch", "whiskers"],
+        "fox": ["mask", "whiskers", "brush_tail"],
+        "wolf": ["mask", "whiskers", "brush_tail"],
+        "bear": ["none", "patch"],
+        "panda": ["eye_patches", "none"],
+        "koala": ["none", "nose_patch"],
+        "tiger": ["stripes", "none"],
+        "lion": ["mane", "none"],
+        "rabbit": ["whiskers", "nose_patch"],
+        "monkey": ["none", "cheek_patches"],
+        "elephant": ["tusks", "none"],
+        "giraffe": ["spots", "none"],
+        "zebra": ["stripes", "none"],
+        "penguin": ["bib", "none"],
+        "owl": ["feather_tufts", "none"],
+        "deer": ["antlers", "spots", "none"],
+        "raccoon": ["mask", "whiskers", "none"],
+        "squirrel": ["bushy_tail", "none"],
+        "hedgehog": ["spikes", "none"],
+        "otter": ["whiskers", "none"],
+        "frog": ["spots", "none"]
+    }
+    
+    EXPRESSIONS = ["happy", "serious", "surprised", "sleepy", "wink"]
+    
+    def __init__(self, seed: Optional[int] = None):
+        """Initialize the avatar generator with an optional seed for reproducibility."""
+        if seed is not None:
+            random.seed(seed)
+    
+    def _get_colors(self, animal: str, color_palette: str) -> List[str]:
+        """Get colors for the given animal and palette."""
+        if color_palette in self.COLOR_PALETTES:
+            if animal in self.COLOR_PALETTES[color_palette]:
+                return self.COLOR_PALETTES[color_palette][animal]
+            elif "all" in self.COLOR_PALETTES[color_palette]:
+                return self.COLOR_PALETTES[color_palette]["all"]
+        
+        # Default to natural palette for the animal or general natural colors
+        if animal in self.COLOR_PALETTES["natural"]:
+            return self.COLOR_PALETTES["natural"][animal]
+        
+        # If no specific colors found, use a mix of browns and grays
+        return ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#808080", "#A9A9A9", "#FFFFFF"]
+    
+    def _get_ear_style(self, animal: str) -> str:
+        """Get a random ear style appropriate for the animal."""
+        if animal in self.EAR_STYLES:
+            return random.choice(self.EAR_STYLES[animal])
+        return "none"  # Default for animals not in the list
+    
+    def _get_special_feature(self, animal: str) -> str:
+        """Get a random special feature appropriate for the animal."""
+        if animal in self.SPECIAL_FEATURES:
+            return random.choice(self.SPECIAL_FEATURES[animal])
+        return "none"  # Default for animals not in the list
+    
+    def _draw_circle(self, cx: float, cy: float, r: float, fill: str, 
+                     stroke: str = "none", stroke_width: float = 1.0) -> str:
+        """Generate SVG for a circle."""
+        return f'<circle cx="{cx}" cy="{cy}" r="{r}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}" />'
+    
+    def _draw_ellipse(self, cx: float, cy: float, rx: float, ry: float, 
+                      fill: str, stroke: str = "none", stroke_width: float = 1.0) -> str:
+        """Generate SVG for an ellipse."""
+        return f'<ellipse cx="{cx}" cy="{cy}" rx="{rx}" ry="{ry}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}" />'
+    
+    def _draw_path(self, d: str, fill: str, stroke: str = "none", 
+                   stroke_width: float = 1.0, stroke_linecap: str = "round") -> str:
+        """Generate SVG for a path."""
+        return f'<path d="{d}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}" stroke-linecap="{stroke_linecap}" />'
+    
+    def _draw_polygon(self, points: str, fill: str, stroke: str = "none", 
+                      stroke_width: float = 1.0) -> str:
+        """Generate SVG for a polygon."""
+        return f'<polygon points="{points}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}" />'
+    
+    def _draw_face(self, animal: str, face_shape: str, face_color: str, 
+                   x: float, y: float, size: float) -> str:
+        """Draw the animal's face based on face shape."""
+        elements = []
+        
+        if face_shape == "round":
+            elements.append(self._draw_circle(x, y, size * 0.4, face_color))
+        elif face_shape == "oval":
+            elements.append(self._draw_ellipse(x, y, size * 0.35, size * 0.45, face_color))
+        elif face_shape == "square":
+            points = (f"{x-size*0.35},{y-size*0.35} {x+size*0.35},{y-size*0.35} "
+                     f"{x+size*0.35},{y+size*0.35} {x-size*0.35},{y+size*0.35}")
+            elements.append(self._draw_polygon(points, face_color))
+        elif face_shape == "heart":
+            # Create a heart shape using paths
+            cx, cy = x, y + size * 0.05
+            r = size * 0.2
+            path = (f"M {cx} {cy-r*0.4} "
+                   f"C {cx-r*1.5} {cy-r*1.5}, {cx-r*2} {cy+r*0.5}, {cx} {cy+r} "
+                   f"C {cx+r*2} {cy+r*0.5}, {cx+r*1.5} {cy-r*1.5}, {cx} {cy-r*0.4} Z")
+            elements.append(self._draw_path(path, face_color))
+        elif face_shape == "triangular":
+            points = f"{x},{y-size*0.4} {x+size*0.4},{y+size*0.3} {x-size*0.4},{y+size*0.3}"
+            elements.append(self._draw_polygon(points, face_color))
+        elif face_shape == "diamond":
+            points = f"{x},{y-size*0.4} {x+size*0.35},{y} {x},{y+size*0.4} {x-size*0.35},{y}"
+            elements.append(self._draw_polygon(points, face_color))
+        
+        return "\n".join(elements)
+    
+    def _draw_ears(self, animal: str, ear_style: str, face_color: str, 
+                   inner_color: str, x: float, y: float, size: float) -> str:
+        """Draw the animal's ears based on ear style."""
+        elements = []
+        
+        if ear_style == "none":
+            return ""
+        
+        if ear_style == "pointed":
+            # Left ear
+            points_left = f"{x-size*0.2},{y-size*0.1} {x-size*0.35},{y-size*0.45} {x-size*0.05},{y-size*0.15}"
+            elements.append(self._draw_polygon(points_left, face_color))
+            
+            # Right ear
+            points_right = f"{x+size*0.2},{y-size*0.1} {x+size*0.35},{y-size*0.45} {x+size*0.05},{y-size*0.15}"
+            elements.append(self._draw_polygon(points_right, face_color))
+            
+            # Inner ears
+            points_inner_left = f"{x-size*0.2},{y-size*0.13} {x-size*0.3},{y-size*0.38} {x-size*0.1},{y-size*0.17}"
+            elements.append(self._draw_polygon(points_inner_left, inner_color))
+            
+            points_inner_right = f"{x+size*0.2},{y-size*0.13} {x+size*0.3},{y-size*0.38} {x+size*0.1},{y-size*0.17}"
+            elements.append(self._draw_polygon(points_inner_right, inner_color))
+            
+        elif ear_style == "round":
+            # Left ear
+            elements.append(self._draw_circle(x-size*0.25, y-size*0.25, size*0.15, face_color))
+            
+            # Right ear
+            elements.append(self._draw_circle(x+size*0.25, y-size*0.25, size*0.15, face_color))
+            
+            # Inner ears
+            elements.append(self._draw_circle(x-size*0.25, y-size*0.25, size*0.08, inner_color))
+            elements.append(self._draw_circle(x+size*0.25, y-size*0.25, size*0.08, inner_color))
+            
+        elif ear_style == "folded" or ear_style == "floppy":
+            # Left ear
+            path_left = (f"M {x-size*0.15} {y-size*0.15} "
+                        f"C {x-size*0.3} {y-size*0.4}, {x-size*0.4} {y-size*0.2}, {x-size*0.35} {y}")
+            elements.append(self._draw_path(path_left, face_color, stroke="none", stroke_width=size*0.08))
+            
+            # Right ear
+            path_right = (f"M {x+size*0.15} {y-size*0.15} "
+                         f"C {x+size*0.3} {y-size*0.4}, {x+size*0.4} {y-size*0.2}, {x+size*0.35} {y}")
+            elements.append(self._draw_path(path_right, face_color, stroke="none", stroke_width=size*0.08))
+            
+        elif ear_style == "long" or ear_style == "standing":
+            # Left ear
+            points_left = f"{x-size*0.2},{y-size*0.15} {x-size*0.3},{y-size*0.6} {x-size*0.05},{y-size*0.15}"
+            elements.append(self._draw_polygon(points_left, face_color))
+            
+            # Right ear
+            points_right = f"{x+size*0.2},{y-size*0.15} {x+size*0.3},{y-size*0.6} {x+size*0.05},{y-size*0.15}"
+            elements.append(self._draw_polygon(points_right, face_color))
+            
+            # Inner ears
+            points_inner_left = f"{x-size*0.18},{y-size*0.18} {x-size*0.25},{y-size*0.5} {x-size*0.1},{y-size*0.18}"
+            elements.append(self._draw_polygon(points_inner_left, inner_color))
+            
+            points_inner_right = f"{x+size*0.18},{y-size*0.18} {x+size*0.25},{y-size*0.5} {x+size*0.1},{y-size*0.18}"
+            elements.append(self._draw_polygon(points_inner_right, inner_color))
+            
+        elif ear_style == "large":
+            # Left ear
+            elements.append(self._draw_ellipse(x-size*0.25, y-size*0.3, size*0.2, size*0.25, face_color))
+            
+            # Right ear
+            elements.append(self._draw_ellipse(x+size*0.25, y-size*0.3, size*0.2, size*0.25, face_color))
+            
+            # Inner ears
+            elements.append(self._draw_ellipse(x-size*0.25, y-size*0.3, size*0.1, size*0.15, inner_color))
+            elements.append(self._draw_ellipse(x+size*0.25, y-size*0.3, size*0.1, size*0.15, inner_color))
+            
+        elif ear_style == "small":
+            # Left ear
+            elements.append(self._draw_ellipse(x-size*0.22, y-size*0.22, size*0.1, size*0.12, face_color))
+            
+            # Right ear
+            elements.append(self._draw_ellipse(x+size*0.22, y-size*0.22, size*0.1, size*0.12, face_color))
+            
+            # Inner ears
+            elements.append(self._draw_ellipse(x-size*0.22, y-size*0.22, size*0.05, size*0.06, inner_color))
+            elements.append(self._draw_ellipse(x+size*0.22, y-size*0.22, size*0.05, size*0.06, inner_color))
+            
+        elif ear_style == "tufted" and animal == "owl":
+            # Left ear tuft
+            points_left = f"{x-size*0.2},{y-size*0.15} {x-size*0.35},{y-size*0.45} {x-size*0.05},{y-size*0.15}"
+            elements.append(self._draw_polygon(points_left, face_color))
+            
+            # Right ear tuft
+            points_right = f"{x+size*0.2},{y-size*0.15} {x+size*0.35},{y-size*0.45} {x+size*0.05},{y-size*0.15}"
+            elements.append(self._draw_polygon(points_right, face_color))
+            
+        elif ear_style == "wide" and animal == "elephant":
+            # Left ear
+            path_left = (f"M {x-size*0.15} {y-size*0.1} "
+                        f"C {x-size*0.5} {y-size*0.2}, {x-size*0.6} {y+size*0.2}, {x-size*0.15} {y+size*0.2}")
+            elements.append(self._draw_path(path_left, face_color, stroke=face_color, stroke_width=size*0.04))
+            
+            # Right ear
+            path_right = (f"M {x+size*0.15} {y-size*0.1} "
+                         f"C {x+size*0.5} {y-size*0.2}, {x+size*0.6} {y+size*0.2}, {x+size*0.15} {y+size*0.2}")
+            elements.append(self._draw_path(path_right, face_color, stroke=face_color, stroke_width=size*0.04))
+            
+        return "\n".join(elements)
+    
+    def _draw_eyes(self, eye_style: str, expression: str, eye_color: str, 
+                   x: float, y: float, size: float) -> str:
+        """Draw the animal's eyes based on eye style and expression."""
+        elements = []
+        eye_spacing = size * 0.2
+        
+        if eye_style == "round":
+            eye_size = size * 0.08
+            # Left eye
+            elements.append(self._draw_circle(x - eye_spacing, y - size * 0.05, eye_size, "#FFFFFF"))
+            elements.append(self._draw_circle(x - eye_spacing, y - size * 0.05, eye_size * 0.6, eye_color))
+            
+            # Right eye
+            elements.append(self._draw_circle(x + eye_spacing, y - size * 0.05, eye_size, "#FFFFFF"))
+            elements.append(self._draw_circle(x + eye_spacing, y - size * 0.05, eye_size * 0.6, eye_color))
+            
+        elif eye_style == "oval":
+            eye_width = size * 0.1
+            eye_height = size * 0.07
+            # Left eye
+            elements.append(self._draw_ellipse(x - eye_spacing, y - size * 0.05, eye_width, eye_height, "#FFFFFF"))
+            elements.append(self._draw_ellipse(x - eye_spacing, y - size * 0.05, eye_width * 0.6, eye_height * 0.6, eye_color))
+            
+            # Right eye
+            elements.append(self._draw_ellipse(x + eye_spacing, y - size * 0.05, eye_width, eye_height, "#FFFFFF"))
+            elements.append(self._draw_ellipse(x + eye_spacing, y - size * 0.05, eye_width * 0.6, eye_height * 0.6, eye_color))
+            
+        elif eye_style == "almond":
+            # Left eye - almond shape
+            path_left = (f"M {x-eye_spacing-size*0.1} {y-size*0.05} "
+                        f"C {x-eye_spacing} {y-size*0.12}, {x-eye_spacing} {y+size*0.02}, {x-eye_spacing+size*0.1} {y-size*0.05} "
+                        f"C {x-eye_spacing} {y+size*0.02}, {x-eye_spacing} {y-size*0.12}, {x-eye_spacing-size*0.1} {y-size*0.05} Z")
+            elements.append(self._draw_path(path_left, "#FFFFFF"))
+            elements.append(self._draw_circle(x - eye_spacing, y - size * 0.05, size * 0.05, eye_color))
+            
+            # Right eye - almond shape
+            path_right = (f"M {x+eye_spacing-size*0.1} {y-size*0.05} "
+                         f"C {x+eye_spacing} {y-size*0.12}, {x+eye_spacing} {y+size*0.02}, {x+eye_spacing+size*0.1} {y-size*0.05} "
+                         f"C {x+eye_spacing} {y+size*0.02}, {x+eye_spacing} {y-size*0.12}, {x+eye_spacing-size*0.1} {y-size*0.05} Z")
+            elements.append(self._draw_path(path_right, "#FFFFFF"))
+            elements.append(self._draw_circle(x + eye_spacing, y - size * 0.05, size * 0.05, eye_color))
+            
+        elif eye_style == "wide":
+            eye_width = size * 0.12
+            eye_height = size * 0.08
+            # Left eye
+            elements.append(self._draw_ellipse(x - eye_spacing, y - size * 0.05, eye_width, eye_height, "#FFFFFF"))
+            elements.append(self._draw_ellipse(x - eye_spacing, y - size * 0.05, eye_width * 0.5, eye_height * 0.6, eye_color))
+            
+            # Right eye
+            elements.append(self._draw_ellipse(x + eye_spacing, y - size * 0.05, eye_width, eye_height, "#FFFFFF"))
+            elements.append(self._draw_ellipse(x + eye_spacing, y - size * 0.05, eye_width * 0.5, eye_height * 0.6, eye_color))
+            
+        elif eye_style == "narrow":
+            eye_width = size * 0.12
+            eye_height = size * 0.04
+            # Left eye
+            elements.append(self._draw_ellipse(x - eye_spacing, y - size * 0.05, eye_width, eye_height, "#FFFFFF"))
+            elements.append(self._draw_ellipse(x - eye_spacing, y - size * 0.05, eye_width * 0.4, eye_height * 0.7, eye_color))
+            
+            # Right eye
+            elements.append(self._draw_ellipse(x + eye_spacing, y - size * 0.05, eye_width, eye_height, "#FFFFFF"))
+            elements.append(self._draw_ellipse(x + eye_spacing, y - size * 0.05, eye_width * 0.4, eye_height * 0.7, eye_color))
+            
+        elif eye_style == "cute":
+            eye_size = size * 0.1
+            # Left eye
+            elements.append(self._draw_circle(x - eye_spacing, y - size * 0.05, eye_size, "#FFFFFF"))
+            elements.append(self._draw_circle(x - eye_spacing - size * 0.02, y - size * 0.07, eye_size * 0.5, eye_color))
+            elements.append(self._draw_circle(x - eye_spacing - size * 0.03, y - size * 0.08, eye_size * 0.15, "#FFFFFF"))
+            
+            # Right eye
+            elements.append(self._draw_circle(x + eye_spacing, y - size * 0.05, eye_size, "#FFFFFF"))
+            elements.append(self._draw_circle(x + eye_spacing - size * 0.02, y - size * 0.07, eye_size * 0.5, eye_color))
+            elements.append(self._draw_circle(x + eye_spacing - size * 0.03, y - size * 0.08, eye_size * 0.15, "#FFFFFF"))
+        
+        # Apply expression
+        if expression == "happy":
+            # Close bottom half of eyes slightly
+            path_left = (f"M {x-eye_spacing-size*0.1} {y-size*0.03} "
+                        f"Q {x-eye_spacing} {y+size*0.02}, {x-eye_spacing+size*0.1} {y-size*0.03}")
+            elements.append(self._draw_path(path_left, face_color, stroke="#000000", stroke_width=size*0.01))
+            
+            path_right = (f"M {x+eye_spacing-size*0.1} {y-size*0.03} "
+                         f"Q {x+eye_spacing} {y+size*0.02}, {x+eye_spacing+size*0.1} {y-size*0.03}")
+            elements.append(self._draw_path(path_right, face_color, stroke="#000000", stroke_width=size*0.01))
+            
+        elif expression == "serious":
+            # Serious eyebrows
+            path_left = (f"M {x-eye_spacing-size*0.08} {y-size*0.12} "
+                        f"L {x-eye_spacing+size*0.08} {y-size*0.15}")
+            elements.append(self._draw_path(path_left, "none", stroke="#000000", stroke_width=size*0.02))
+            
+            path_right = (f"M {x+eye_spacing-size*0.08} {y-size*0.15} "
+                         f"L {x+eye_spacing+size*0.08} {y-size*0.12}")
+            elements.append(self._draw_path(path_right, "none", stroke="#000000", stroke_width=size*0.02))
+            
+        elif expression == "surprised":
+            # Raise eyebrows
+            path_left = (f"M {x-eye_spacing-size*0.08} {y-size*0.15} "
+                        f"Q {x-eye_spacing} {y-size*0.18}, {x-eye_spacing+size*0.08} {y-size*0.15}")
+            elements.append(self._draw_path(path_left, "none", stroke="#000000", stroke_width=size*0.015))
+            
+            path_right = (f"M {x+eye_spacing-size*0.08} {y-size*0.15} "
+                         f"Q {x+eye_spacing} {y-size*0.18}, {x+eye_spacing+size*0.08} {y-size*0.15}")
+            elements.append(self._draw_path(path_right, "none", stroke="#000000", stroke_width=size*0.015))
+            
+        elif expression == "sleepy":
+            # Half-closed eyes
+            path_left = (f"M {x-eye_spacing-size*0.1} {y-size*0.08} "
+                        f"Q {x-eye_spacing} {y-size*0.01}, {x-eye_spacing+size*0.1} {y-size*0.08}")
+            elements.append(self._draw_path(path_left, "none", stroke="#000000", stroke_width=size*0.015))
+            
+            path_right = (f"M {x+eye_spacing-size*0.1} {y-size*0.08} "
+                         f"Q {x+eye_spacing} {y-size*0.01}, {x+eye_spacing+size*0.1} {y-size*0.08}")
+            elements.append(self._draw_path(path_right, "none", stroke="#000000", stroke_width=size*0.015))
+            
+        elif expression == "wink":
+            # Right eye normal
+            # Left eye winking
+            path_left = (f"M {x-eye_spacing-size*0.1} {y-size*0.05} "
+                        f"Q {x-eye_spacing} {y-size*0.1}, {x-eye_spacing+size*0.1} {y-size*0.05}")
+            elements.append(self._draw_path(path_left, "none", stroke="#000000", stroke_width=size*0.02))
+            
+        return "\n".join(elements)
+    
+    def _draw_nose(self, animal: str, nose_style: str, nose_color: str, 
+                   x: float, y: float, size: float) -> str:
+        """Draw the animal's nose based on nose style."""
+        elements = []
+        
+        if nose_style == "round":
+            elements.append(self._draw_circle(x, y + size * 0.05, size * 0.08, nose_color))
+            
+        elif nose_style == "triangular":
+            points = f"{x},{y+size*0.02} {x-size*0.08},{y+size*0.12} {x+size*0.08},{y+size*0.12}"
+            elements.append(self._draw_polygon(points, nose_color))
+            
+        elif nose_style == "small":
+            elements.append(self._draw_circle(x, y + size * 0.05, size * 0.05, nose_color))
+            
+        elif nose_style == "large":
+            if animal in ["dog", "bear", "panda", "koala"]:
+                elements.append(self._draw_ellipse(x, y + size * 0.05, size * 0.1, size * 0.08, nose_color))
+            else:
+                elements.append(self._draw_circle(x, y + size * 0.05, size * 0.1, nose_color))
+                
+        elif nose_style == "heart":
+            # Heart-shaped nose
+            cx, cy = x, y + size * 0.05
+            r = size * 0.06
+            path = (f"M {cx} {cy-r*0.2} "
+                   f"C {cx-r*1.5} {cy-r*1.2}, {cx-r*1.8} {cy+r*0.6}, {cx} {cy+r*0.8} "
+                   f"C {cx+r*1.8} {cy+r*0.6}, {cx+r*1.5} {cy-r*1.2}, {cx} {cy-r*0.2} Z")
+            elements.append(self._draw_path(path, nose_color))
+            
+        elif nose_style == "button":
+            elements.append(self._draw_circle(x, y + size * 0.05, size * 0.06, nose_color))
+            elements.append(self._draw_circle(x, y + size * 0.05, size * 0.04, nose_color, stroke="#000000", stroke_width=size*0.01))
+            
+        return "\n".join(elements)
+    
+    def _draw_mouth(self, expression: str, x: float, y: float, size: float) -> str:
+        """Draw the animal's mouth based on expression."""
+        elements = []
+        
+        if expression == "happy":
+            path = (f"M {x-size*0.15} {y+size*0.12} "
+                   f"Q {x} {y+size*0.25}, {x+size*0.15} {y+size*0.12}")
+            elements.append(self._draw_path(path, "none", stroke="#000000", stroke_width=size*0.02))
+            
+        elif expression == "serious":
+            path = (f"M {x-size*0.12} {y+size*0.15} "
+                   f"L {x+size*0.12} {y+size*0.15}")
+            elements.append(self._draw_path(path, "none", stroke="#000000", stroke_width=size*0.02))
+            
+        elif expression == "surprised":
+            elements.append(self._draw_ellipse(x, y + size * 0.15, size * 0.06, size * 0.08, "#FFFFFF", 
+                                            stroke="#000000", stroke_width=size*0.01))
+            
+        elif expression == "sleepy":
+            path = (f"M {x-size*0.08} {y+size*0.15} "
+                   f"Q {x} {y+size*0.12}, {x+size*0.08} {y+size*0.15}")
+            elements.append(self._draw_path(path, "none", stroke="#000000", stroke_width=size*0.015))
+            
+        elif expression == "wink":
+            path = (f"M {x-size*0.15} {y+size*0.12} "
+                   f"Q {x} {y+size*0.22}, {x+size*0.15} {y+size*0.12}")
+            elements.append(self._draw_path(path, "none", stroke="#000000", stroke_width=size*0.02))
+            
+        return "\n".join(elements)
+    
+    def _draw_special_features(self, animal: str, special_feature: str, face_color: str, 
+                              accent_color: str, x: float, y: float, size: float) -> str:
+        """Draw special features based on animal and feature type."""
+        elements = []
+        
+        if special_feature == "none":
+            return ""
+            
+        elif special_feature == "whiskers":
+            # Left whiskers
+            for i in range(3):
+                angle = -30 + i * 30
+                length = size * 0.25
+                end_x = x - size * 0.15 + length * math.cos(math.radians(angle))
+                end_y = y + size * 0.05 + length * math.sin(math.radians(angle))
+                elements.append(self._draw_path(
+                    f"M {x-size*0.15} {y+size*0.05} L {end_x} {end_y}", 
+                    "none", stroke="#000000", stroke_width=size*0.01))
+            
+            # Right whiskers
+            for i in range(3):
+                angle = -150 + i * 30
+                length = size * 0.25
+                end_x = x + size * 0.15 + length * math.cos(math.radians(angle))
+                end_y = y + size * 0.05 + length * math.sin(math.radians(angle))
+                elements.append(self._draw_path(
+                    f"M {x+size*0.15} {y+size*0.05} L {end_x} {end_y}", 
+                    "none", stroke="#000000", stroke_width=size*0.01))
+                    
+        elif special_feature == "stripes":
+            # Vertical stripes
+            if animal == "tiger":
+                for i in range(3):
+                    offset = -size * 0.2 + i * size * 0.2
+                    path = (f"M {x+offset} {y-size*0.3} "
+                           f"Q {x+offset+size*0.1} {y}, {x+offset} {y+size*0.3}")
+                    elements.append(self._draw_path(path, "none", stroke=accent_color, stroke_width=size*0.04))
+            # Horizontal stripes for zebra
+            elif animal == "zebra":
+                for i in range(3):
+                    offset = -size * 0.2 + i * size * 0.2
+                    path = (f"M {x-size*0.3} {y+offset} "
+                           f"Q {x} {y+offset+size*0.1}, {x+size*0.3} {y+offset}")
+                    elements.append(self._draw_path(path, "none", stroke=accent_color, stroke_width=size*0.04))
+                    
+        elif special_feature == "spots":
+            # Random spots
+            num_spots = random.randint(3, 6)
+            for _ in range(num_spots):
+                spot_x = x + random.uniform(-size * 0.3, size * 0.3)
+                spot_y = y + random.uniform(-size * 0.3, size * 0.3)
+                spot_size = random.uniform(size * 0.03, size * 0.08)
+                elements.append(self._draw_circle(spot_x, spot_y, spot_size, accent_color))
+                
+        elif special_feature == "patch":
+            # Eye patch or face patch
+            if animal == "dog":
+                elements.append(self._draw_ellipse(x - size * 0.2, y, size * 0.2, size * 0.25, accent_color))
+            else:
+                # Generic face patch
+                elements.append(self._draw_ellipse(x, y + size * 0.2, size * 0.2, size * 0.15, accent_color))
+                
+        elif special_feature == "mask":
+            if animal == "raccoon":
+                # Raccoon mask
+                path = (f"M {x-size*0.3} {y-size*0.1} "
+                       f"Q {x} {y-size*0.3}, {x+size*0.3} {y-size*0.1} "
+                       f"Q {x+size*0.2} {y+size*0.1}, {x} {y+size*0.15} "
+                       f"Q {x-size*0.2} {y+size*0.1}, {x-size*0.3} {y-size*0.1} Z")
+                elements.append(self._draw_path(path, accent_color))
+            elif animal in ["fox", "wolf"]:
+                # Fox/wolf mask
+                path = (f"M {x-size*0.3} {y-size*0.1} "
+                       f"L {x} {y+size*0.1} "
+                       f"L {x+size*0.3} {y-size*0.1} "
+                       f"Q {x+size*0.15} {y-size*0.05}, {x} {y-size*0.1} "
+                       f"Q {x-size*0.15} {y-size*0.05}, {x-size*0.3} {y-size*0.1} Z")
+                elements.append(self._draw_path(path, accent_color))
+                
+        elif special_feature == "eye_patches" and animal == "panda":
+            # Panda eye patches
+            elements.append(self._draw_ellipse(x - size * 0.2, y - size * 0.05, size * 0.15, size * 0.2, accent_color))
+            elements.append(self._draw_ellipse(x + size * 0.2, y - size * 0.05, size * 0.15, size * 0.2, accent_color))
+            
+        elif special_feature == "nose_patch":
+            elements.append(self._draw_ellipse(x, y + size * 0.05, size * 0.12, size * 0.1, accent_color))
+            
+        elif special_feature == "mane" and animal == "lion":
+            # Lion mane
+            for i in range(12):
+                angle = i * 30
+                outer_x = x + size * 0.5 * math.cos(math.radians(angle))
+                outer_y = y + size * 0.5 * math.sin(math.radians(angle))
+                inner_x = x + size * 0.3 * math.cos(math.radians(angle))
+                inner_y = y + size * 0.3 * math.sin(math.radians(angle))
+                
+                # Draw mane sections
+                path = (f"M {inner_x} {inner_y} "
+                       f"L {outer_x} {outer_y} "
+                       f"A {size*0.5} {size*0.5} 0 0 1 "
+                       f"{x + size * 0.5 * math.cos(math.radians(angle + 30))} "
+                       f"{y + size * 0.5 * math.sin(math.radians(angle + 30))} "
+                       f"L {x + size * 0.3 * math.cos(math.radians(angle + 30))} "
+                       f"{y + size * 0.3 * math.sin(math.radians(angle + 30))} Z")
+                elements.append(self._draw_path(path, accent_color))
+                
+        elif special_feature == "tusks" and animal == "elephant":
+            # Elephant tusks
+            path_left = (f"M {x-size*0.15} {y+size*0.1} "
+                        f"Q {x-size*0.3} {y+size*0.3}, {x-size*0.35} {y+size*0.5}")
+            elements.append(self._draw_path(path_left, "#FFFFF0", stroke="#F5F5DC", stroke_width=size*0.04))
+            
+            path_right = (f"M {x+size*0.15} {y+size*0.1} "
+                         f"Q {x+size*0.3} {y+size*0.3}, {x+size*0.35} {y+size*0.5}")
+            elements.append(self._draw_path(path_right, "#FFFFF0", stroke="#F5F5DC", stroke_width=size*0.04))
+            
+        elif special_feature == "antlers" and animal == "deer":
+            # Deer antlers
+            # Left antler
+            path_left = (f"M {x-size*0.15} {y-size*0.2} "
+                        f"L {x-size*0.3} {y-size*0.45} "
+                        f"L {x-size*0.4} {y-size*0.4} "
+                        f"M {x-size*0.3} {y-size*0.45} "
+                        f"L {x-size*0.2} {y-size*0.5}")
+            elements.append(self._draw_path(path_left, "none", stroke=accent_color, stroke_width=size*0.03))
+            
+            # Right antler
+            path_right = (f"M {x+size*0.15} {y-size*0.2} "
+                         f"L {x+size*0.3} {y-size*0.45} "
+                         f"L {x+size*0.4} {y-size*0.4} "
+                         f"M {x+size*0.3} {y-size*0.45} "
+                         f"L {x+size*0.2} {y-size*0.5}")
+            elements.append(self._draw_path(path_right, "none", stroke=accent_color, stroke_width=size*0.03))
+            
+        elif special_feature == "bushy_tail" and animal == "squirrel":
+            # Squirrel bushy tail
+            path = (f"M {x+size*0.1} {y+size*0.2} "
+                   f"Q {x+size*0.5} {y}, {x+size*0.3} {y-size*0.3} "
+                   f"Q {x+size*0.4} {y-size*0.4}, {x+size*0.5} {y-size*0.35} "
+                   f"Q {x+size*0.45} {y-size*0.25}, {x+size*0.6} {y-size*0.3} "
+                   f"Q {x+size*0.55} {y-size*0.1}, {x+size*0.4} {y+size*0.1} "
+                   f"Q {x+size*0.3} {y+size*0.3}, {x+size*0.1} {y+size*0.2} Z")
+            elements.append(self._draw_path(path, accent_color))
+            
+        elif special_feature == "brush_tail" and animal in ["fox", "wolf"]:
+            # Fox/wolf brush tail
+            path = (f"M {x+size*0.1} {y+size*0.2} "
+                   f"Q {x+size*0.4} {y+size*0.1}, {x+size*0.5} {y-size*0.1} "
+                   f"Q {x+size*0.6} {y-size*0.2}, {x+size*0.7} {y-size*0.1} "
+                   f"Q {x+size*0.65} {y}, {x+size*0.6} {y+size*0.1} "
+                   f"Q {x+size*0.5} {y+size*0.2}, {x+size*0.3} {y+size*0.3} Z")
+            elements.append(self._draw_path(path, face_color))
+            # Tail tip
+            elements.append(self._draw_ellipse(x + size * 0.6, y - size * 0.05, size * 0.12, size * 0.08, accent_color))
+            
+        elif special_feature == "bib" and animal == "penguin":
+            # Penguin bib/chest
+            path = (f"M {x-size*0.2} {y} "
+                   f"Q {x} {y+size*0.4}, {x+size*0.2} {y} "
+                   f"Q {x} {y+size*0.1}, {x-size*0.2} {y} Z")
+            elements.append(self._draw_path(path, "#FFFFFF"))
+            
+        elif special_feature == "feather_tufts" and animal == "owl":
+            # Owl feather tufts
+            path_left = (f"M {x-size*0.1} {y-size*0.3} "
+                        f"Q {x-size*0.15} {y-size*0.45}, {x-size*0.05} {y-size*0.5}")
+            elements.append(self._draw_path(path_left, face_color, stroke="#000000", stroke_width=size*0.01))
+            
+            path_right = (f"M {x+size*0.1} {y-size*0.3} "
+                         f"Q {x+size*0.15} {y-size*0.45}, {x+size*0.05} {y-size*0.5}")
+            elements.append(self._draw_path(path_right, face_color, stroke="#000000", stroke_width=size*0.01))
+            
+        elif special_feature == "spikes" and animal == "hedgehog":
+            # Hedgehog spikes
+            for i in range(12):
+                angle = i * 30
+                inner_x = x + size * 0.3 * math.cos(math.radians(angle))
+                inner_y = y + size * 0.3 * math.sin(math.radians(angle))
+                outer_x = x + size * 0.5 * math.cos(math.radians(angle))
+                outer_y = y + size * 0.5 * math.sin(math.radians(angle))
+                
+                path = f"M {inner_x} {inner_y} L {outer_x} {outer_y}"
+                elements.append(self._draw_path(path, "none", stroke=accent_color, stroke_width=size*0.03))
+                
+        elif special_feature == "cheek_patches" and animal == "monkey":
+            # Monkey cheek patches
+            elements.append(self._draw_circle(x - size * 0.25, y + size * 0.1, size * 0.12, accent_color))
+            elements.append(self._draw_circle(x + size * 0.25, y + size * 0.1, size * 0.12, accent_color))
+            
+        return "\n".join(elements)
+    
+    def generate_avatar(self, animal: Optional[str] = None, color_palette: str = "natural",
+                       face_shape: Optional[str] = None, eye_style: Optional[str] = None,
+                       ear_style: Optional[str] = None, nose_style: Optional[str] = None,
+                       expression: Optional[str] = None, special_feature: Optional[str] = None,
+                       size: int = 500) -> str:
+        """
+        Generate an animal avatar with the specified parameters.
+        
+        Args:
+            animal: Animal type (e.g., "cat", "dog"). If None, a random animal is selected.
+            color_palette: Color palette to use (e.g., "natural", "pastel", "vibrant", "mono").
+            face_shape: Shape of the face. If None, a random shape is selected.
+            eye_style: Style of the eyes. If None, a random style is selected.
+            ear_style: Style of the ears. If None, a random style is selected for the animal.
+            nose_style: Style of the nose. If None, a random style is selected.
+            expression: Facial expression. If None, a random expression is selected.
+            special_feature: Special feature to add. If None, a random feature is selected for the animal.
+            size: Size of the avatar in pixels.
+            
+        Returns:
+            SVG string representation of the generated avatar.
+        """
+        # Select random animal if not specified
+        if animal is None or animal not in self.ANIMALS:
+            animal = random.choice(self.ANIMALS)
+        
+        # Select random options if not specified
+        if face_shape is None or face_shape not in self.FACE_SHAPES:
+            face_shape = random.choice(self.FACE_SHAPES)
+        
+        if eye_style is None or eye_style not in self.EYE_STYLES:
+            eye_style = random.choice(self.EYE_STYLES)
+        
+        if ear_style is None:
+            ear_style = self._get_ear_style(animal)
+        
+        if nose_style is None or nose_style not in self.NOSE_STYLES:
+            nose_style = random.choice(self.NOSE_STYLES)
+        
+        if expression is None or expression not in self.EXPRESSIONS:
+            expression = random.choice(self.EXPRESSIONS)
+        
+        if special_feature is None:
+            special_feature = self._get_special_feature(animal)
+        
+        # Get colors
+        colors = self._get_colors(animal, color_palette)
+        face_color = random.choice(colors)
+        
+        # Make sure accent color is different from face color
+        remaining_colors = [c for c in colors if c != face_color]
+        if not remaining_colors:
+            remaining_colors = ["#000000", "#FFFFFF"]
+        accent_color = random.choice(remaining_colors)
+        
+        # Ensure inner ear color is different from face color
+        inner_ear_color = random.choice(remaining_colors)
+        
+        # Eye color options
+        eye_colors = ["#000000", "#331800", "#0000FF", "#008000", "#FFA500", "#800080"]
+        eye_color = random.choice(eye_colors)
+        
+        # Nose color options based on animal
+        if animal in ["dog", "cat", "fox", "wolf", "bear", "panda", "koala", "tiger", "lion"]:
+            nose_color = "#000000"
+        else:
+            nose_color = accent_color
+        
+        # Center coordinates
+        x, y = size / 2, size / 2
+        
+        # Generate SVG elements
+        elements = []
+        
+        # Draw the face first
+        elements.append(self._draw_face(animal, face_shape, face_color, x, y, size))
+        
+        # Draw special features behind the face if needed
+        if special_feature in ["mane", "spikes"]:
+            elements.append(self._draw_special_features(animal, special_feature, face_color, 
+                                                     accent_color, x, y, size))
+        
+        # Draw ears
+        elements.append(self._draw_ears(animal, ear_style, face_color, inner_ear_color, x, y, size))
+        
+        # Draw eyes
+        elements.append(self._draw_eyes(eye_style, expression, eye_color, x, y, size))
+        
+        # Draw nose
+        elements.append(self._draw_nose(animal, nose_style, nose_color, x, y, size))
+        
+        # Draw mouth
+        elements.append(self._draw_mouth(expression, x, y, size))
+        
+        # Draw special features that should be in front
+        if special_feature not in ["mane", "spikes"]:
+            elements.append(self._draw_special_features(animal, special_feature, face_color, 
+                                                     accent_color, x, y, size))
+        
+        # Assemble SVG
+        svg_content = '\n'.join(elements)
+        svg = (f'<svg viewBox="0 0 {size} {size}" xmlns="http://www.w3.org/2000/svg">\n'
+               f'{svg_content}\n'
+               f'</svg>')
+        
+        return svg
+    
+    def get_avatar_options(self) -> Dict[str, List[str]]:
+        """Return all available avatar options."""
+        return {
+            "animals": self.ANIMALS,
+            "color_palettes": list(self.COLOR_PALETTES.keys()),
+            "face_shapes": self.FACE_SHAPES,
+            "eye_styles": self.EYE_STYLES,
+            "ear_styles": {animal: styles for animal, styles in self.EAR_STYLES.items()},
+            "nose_styles": self.NOSE_STYLES,
+            "expressions": self.EXPRESSIONS,
+            "special_features": {animal: features for animal, features in self.SPECIAL_FEATURES.items()}
+        }
+    
+    def generate_random_avatar(self, size: int = 500) -> str:
+        """Generate a completely random avatar."""
+        return self.generate_avatar(size=size)
+
+def generate_avatar_with_options(options: Dict) -> str:
+    """Generate an avatar with the given options."""
+    generator = AnimalAvatarGenerator(seed=options.get("seed"))
+    
+    return generator.generate_avatar(
+        animal=options.get("animal"),
+        color_palette=options.get("color_palette", "natural"),
+        face_shape=options.get("face_shape"),
+        eye_style=options.get("eye_style"),
+        ear_style=options.get("ear_style"),
+        nose_style=options.get("nose_style"),
+        expression=options.get("expression"),
+        special_feature=options.get("special_feature"),
+        size=options.get("size", 500)
+    )
+
+def list_avatar_options() -> Dict[str, List[str]]:
+    """Return all available avatar options."""
+    generator = AnimalAvatarGenerator()
+    return generator.get_avatar_options()
+
+def create_avatar_app():
+    """Command-line interface for the avatar generator."""
+    parser = argparse.ArgumentParser(description="Generate animal avatars")
+    parser.add_argument("--animal", help="Animal type", choices=AnimalAvatarGenerator.ANIMALS)
+    parser.add_argument("--color-palette", help="Color palette", default="natural",
+                        choices=["natural", "pastel", "vibrant", "mono"])
+    parser.add_argument("--face-shape", help="Face shape", choices=AnimalAvatarGenerator.FACE_SHAPES)
+    parser.add_argument("--eye-style", help="Eye style", choices=AnimalAvatarGenerator.EYE_STYLES)
+    parser.add_argument("--ear-style", help="Ear style")
+    parser.add_argument("--nose-style", help="Nose style", choices=AnimalAvatarGenerator.NOSE_STYLES)
+    parser.add_argument("--expression", help="Expression", choices=AnimalAvatarGenerator.EXPRESSIONS)
+    parser.add_argument("--special-feature", help="Special feature")
+    parser.add_argument("--size", help="Size in pixels", type=int, default=500)
+    parser.add_argument("--seed", help="Random seed for reproducibility", type=int)
+    parser.add_argument("--output", help="Output file path", default="avatar.svg")
+    parser.add_argument("--list-options", help="List all available options", action="store_true")
+    
+    args = parser.parse_args()
+    
+    if args.list_options:
+        options = list_avatar_options()
+        print(json.dumps(options, indent=2))
+        return
+    
+    generator = AnimalAvatarGenerator(seed=args.seed)
+    
+    svg = generator.generate_avatar(
+        animal=args.animal,
+        color_palette=args.color_palette,
+        face_shape=args.face_shape,
+        eye_style=args.eye_style,
+        ear_style=args.ear_style,
+        nose_style=args.nose_style,
+        expression=args.expression,
+        special_feature=args.special_feature,
+        size=args.size
+    )
+    
+    with open(args.output, "w") as f:
+        f.write(svg)
+    
+    print(f"Avatar saved to {args.output}")
+
+if __name__ == "__main__":
+    create_avatar_app()