# WidgetBot


**Site index:** [https://2plot.dev/llms.txt](https://2plot.dev/llms.txt) — every page on this site, as Markdown.  
**Network index:** [https://2plot.ai/llms.txt](https://2plot.ai/llms.txt) — The 2plot network; start here to discover sibling sites.  
**Sibling sites:** 11 more in The 2plot network — listed in the site index above.  
**Sitemap:** https://2plot.dev/sitemap.xml  

`dash-widgetbot` integrates Discord chat into your Dash applications using a pure Python hook-based architecture — no React build required.

- **DiscordCrate** — Floating chat button (the one in the bottom-right corner of this page)
- **DiscordWidget** — Inline iframe embed for placing chat anywhere in your layout
- **Slash Commands** — AI-powered `/ai`, `/ask`, `/gen`, `/status` commands
- **Webhook API** — Send messages to Discord from Python

> **Two layers, two deployment stories.** The Crate/Widget is a client-side iframe with no server-side Discord API calls — drop it in and it just works in dev and prod. The slash-command/bot layer is where production pain lives (outbound proxies, rate limits, gunicorn tuning, global vs guild command scope). See the **Production Deployment Guide** section below for the full playbook.

## Installation

[Visit GitHub Repo](https://github.com/pip-install-python/dash-widgetbot)

```bash
pip install dash-widgetbot
pip install dash-widgetbot[bot,ai]   # with Discord bot + Gemini AI
```

---

## Overview



```python
# File: docs/dash_widgetbot/introduction.py

import os
import dash_mantine_components as dmc
from dash_iconify import DashIconify
from dash_widgetbot import discord_widget_container

component = dmc.Stack([
    dmc.Alert(
        children=dmc.Stack([
            dmc.Text(
                "The floating Discord button in the bottom-right corner of this page is a "
                "DiscordCrate — a global chat widget that appears on every page. "
                "Try clicking it to open the community chat!",
                size="sm",
            ),
            dmc.Text(
                "Below is a DiscordWidget — an inline embed you can place anywhere in your layout.",
                size="sm",
                fw=500,
            ),
        ], gap="xs"),
        title="Two Components",
        color="indigo",
        icon=DashIconify(icon="ic:baseline-discord", width=20),
    ),
    dmc.SimpleGrid(
        cols=2,
        children=[
            dmc.Paper(
                dmc.Stack([
                    dmc.Group([
                        DashIconify(icon="tabler:message-circle", width=24, color="#5865f2"),
                        dmc.Text("DiscordCrate", fw=700, size="lg"),
                    ], gap="xs"),
                    dmc.Text("Floating chat button", size="sm", c="dimmed"),
                    dmc.List([
                        dmc.ListItem("Global — appears on every page", icon=DashIconify(icon="tabler:check", width=16, color="teal")),
                        dmc.ListItem("Toggle, notify, navigate commands", icon=DashIconify(icon="tabler:check", width=16, color="teal")),
                        dmc.ListItem("Slash command bridge (/ai, /ask)", icon=DashIconify(icon="tabler:check", width=16, color="teal")),
                        dmc.ListItem("Event listeners (sentMessage, signIn)", icon=DashIconify(icon="tabler:check", width=16, color="teal")),
                    ], size="sm", spacing="xs"),
                ], gap="sm"),
                withBorder=True, p="lg",
            ),
            dmc.Paper(
                dmc.Stack([
                    dmc.Group([
                        DashIconify(icon="tabler:layout-bottombar", width=24, color="#5865f2"),
                        dmc.Text("DiscordWidget", fw=700, size="lg"),
                    ], gap="xs"),
                    dmc.Text("Inline iframe embed", size="sm", c="dimmed"),
                    dmc.List([
                        dmc.ListItem("Placed anywhere in your layout", icon=DashIconify(icon="tabler:check", width=16, color="teal")),
                        dmc.ListItem("No floating button", icon=DashIconify(icon="tabler:check", width=16, color="teal")),
                        dmc.ListItem("Read-only events (message, signIn)", icon=DashIconify(icon="tabler:check", width=16, color="teal")),
                        dmc.ListItem("No command dispatch", icon=DashIconify(icon="tabler:x", width=16, color="gray")),
                    ], size="sm", spacing="xs"),
                ], gap="sm"),
                withBorder=True, p="lg",
            ),
        ],
    ),
    dmc.Paper(
        discord_widget_container(
            server=os.getenv("WIDGETBOT_SERVER", ""),
            channel=os.getenv("WIDGETBOT_CHANNEL", ""),
            width="100%",
            height="350px",
        ),
        withBorder=True, p="xs", radius="md",
    ),
], gap="md")
```


---

## Inline Widget Embed



```python
# File: docs/dash_widgetbot/widget_example.py

import os
import dash_mantine_components as dmc
from dash_widgetbot import discord_widget_container

component = dmc.Stack([
    dmc.Text("Inline Discord Widget", fw=600, size="lg"),
    dmc.Text(
        "discord_widget_container() creates a cross-origin iframe pointing to the "
        "WidgetBot shard. Unlike the Crate, it renders directly in your layout with "
        "no floating button. Configure server, channel, dimensions, and shard URL.",
        size="sm", c="dimmed",
    ),
    dmc.Paper(
        discord_widget_container(
            server=os.getenv("WIDGETBOT_SERVER", ""),
            channel=os.getenv("WIDGETBOT_CHANNEL", ""),
            width="100%",
            height="400px",
        ),
        withBorder=True, p="xs", radius="md",
    ),
    dmc.Code(
        """from dash_widgetbot import discord_widget_container

widget = discord_widget_container(
    server=os.getenv("WIDGETBOT_SERVER"),
    channel=os.getenv("WIDGETBOT_CHANNEL"),
    width="100%",
    height="400px",
    shard="https://e-business.widgetbot.co",  # optional custom shard
)""",
        block=True,
    ),
], gap="md")
```


---

## Sending Messages (Webhook)



```python
# File: docs/dash_widgetbot/webhook_example.py

import os
import time
import dash_mantine_components as dmc
from dash import html, callback, Input, Output, State, clientside_callback, no_update
from dash_iconify import DashIconify
from dash_widgetbot.webhook import send_webhook_message

component = dmc.Stack([
    dmc.Text("Send Messages via Webhook", fw=600, size="lg"),
    dmc.Text(
        "Use send_webhook_message() to post messages to Discord from your Dash app. "
        "Messages are sent via the Discord webhook API and appear in the channel.",
        size="sm", c="dimmed",
    ),
    dmc.Paper(
        dmc.Stack([
            dmc.Textarea(
                id="wb-webhook-content",
                label="Message",
                placeholder="Type a message to send to Discord...",
                value="Hello from pip-docs+ documentation!",
                minRows=2,
                autosize=True,
            ),
            dmc.Group([
                dmc.TextInput(
                    id="wb-webhook-username",
                    placeholder="Bot display name (optional)",
                    label="Username",
                    style={"flex": 1},
                ),
                dmc.TextInput(
                    id="wb-webhook-avatar",
                    placeholder="https://cdn.discordapp.com/embed/avatars/0.png",
                    label="Avatar URL (optional)",
                    style={"flex": 1},
                ),
            ]),
            dmc.Group([
                dmc.Button(
                    "Send to Discord",
                    id="wb-webhook-send-btn",
                    leftSection=DashIconify(icon="tabler:send", width=18),
                    color="indigo",
                    loading=False,
                ),
                html.Div(id="wb-webhook-result"),
            ]),
            dmc.Text(
                "Open the floating Discord chat (bottom-right) to see your message appear.",
                size="xs", c="dimmed", fs="italic",
            ),
        ], gap="sm"),
        withBorder=True, p="lg",
    ),
    dmc.Code(
        """from dash_widgetbot.webhook import send_webhook_message

result = send_webhook_message(
    "Hello from Dash!",
    webhook_url=os.getenv("DISCORD_WEBHOOK_URL"),  # auto from .env
    username="My Bot",          # optional display name
    avatar_url="https://...",   # optional avatar
    thread_id="...",            # optional thread
)
# result: {"success": True, "message_id": "123...", "status_code": 200}""",
        block=True,
    ),
], gap="md")


@callback(
    Output("wb-webhook-result", "children"),
    Input("wb-webhook-send-btn", "n_clicks"),
    State("wb-webhook-content", "value"),
    State("wb-webhook-username", "value"),
    State("wb-webhook-avatar", "value"),
    running=[(Output("wb-webhook-send-btn", "loading"), True, False)],
    prevent_initial_call=True,
)
def send_message(_n, content, username, avatar_url):
    if not content:
        return dmc.Badge("Enter a message first", color="yellow", variant="light", size="sm")

    result = send_webhook_message(
        content,
        username=username or None,
        avatar_url=avatar_url or None,
    )

    if result["success"]:
        return dmc.Badge(
            f"Sent! Open the Discord chat to see it.",
            color="green", variant="light", size="sm",
        )
    return dmc.Badge(
        f"Error: {result.get('error', 'Unknown')[:60]}",
        color="red", variant="light", size="sm",
    )
```


---

## Command Bridge Patterns



```python
# File: docs/dash_widgetbot/commands_example.py

import dash_mantine_components as dmc
from dash_iconify import DashIconify

component = dmc.Stack([
    dmc.Text("Command Bridge Patterns", fw=600, size="lg"),
    dmc.Text(
        "The Crate is controlled via a store-based bridge. Python callbacks write "
        "command dicts to a dcc.Store, and clientside JS dispatches them to the "
        "Crate API. All helpers return command dicts with a _ts timestamp to "
        "prevent Dash deduplication.",
        size="sm", c="dimmed",
    ),
    dmc.Alert(
        "Try clicking the floating Discord button (bottom-right) and typing "
        "/status to see the command bridge in action!",
        title="Live Demo",
        color="teal",
        icon=DashIconify(icon="tabler:terminal-2", width=20),
    ),
    dmc.SimpleGrid(
        cols=2,
        children=[
            dmc.Paper(
                dmc.Stack([
                    dmc.Text("Toggle Open/Close", fw=600, size="sm"),
                    dmc.Code(
                        """from dash_widgetbot import STORE_IDS, crate_toggle

@callback(
    Output(STORE_IDS["command"], "data",
           allow_duplicate=True),
    Input("my-button", "n_clicks"),
    prevent_initial_call=True,
)
def toggle(_n):
    return crate_toggle()      # toggle
    # return crate_toggle(True)  # force open
    # return crate_toggle(False) # force close""",
                        block=True,
                    ),
                ], gap="xs"),
                withBorder=True, p="md",
            ),
            dmc.Paper(
                dmc.Stack([
                    dmc.Text("Show Notification", fw=600, size="sm"),
                    dmc.Code(
                        """from dash_widgetbot import crate_notify

return crate_notify(
    "Hello from Dash!",
    timeout=5000,        # auto-dismiss in 5s
    avatar="https://...",  # custom avatar
)""",
                        block=True,
                    ),
                ], gap="xs"),
                withBorder=True, p="md",
            ),
            dmc.Paper(
                dmc.Stack([
                    dmc.Text("Navigate Channel", fw=600, size="sm"),
                    dmc.Code(
                        """from dash_widgetbot import crate_navigate

# Switch to a different channel
return crate_navigate("CHANNEL_ID")

# Switch server + channel
return crate_navigate(
    "CHANNEL_ID", guild="SERVER_ID"
)""",
                        block=True,
                    ),
                ], gap="xs"),
                withBorder=True, p="md",
            ),
            dmc.Paper(
                dmc.Stack([
                    dmc.Text("Hide / Show", fw=600, size="sm"),
                    dmc.Code(
                        """from dash_widgetbot import crate_hide, crate_show

# Hide the Crate button entirely
return crate_hide()

# Show it again
return crate_show()""",
                        block=True,
                    ),
                ], gap="xs"),
                withBorder=True, p="md",
            ),
        ],
    ),
    dmc.Paper(
        dmc.Stack([
            dmc.Text("All Bridge Helpers", fw=600, size="sm"),
            dmc.Table(
                [
                    dmc.TableThead(dmc.TableTr([
                        dmc.TableTh("Helper"),
                        dmc.TableTh("Description"),
                    ])),
                    dmc.TableTbody([
                        dmc.TableTr([dmc.TableTd(dmc.Code("crate_toggle(is_open)")), dmc.TableTd("Toggle or set open/closed")]),
                        dmc.TableTr([dmc.TableTd(dmc.Code("crate_notify(content, timeout)")), dmc.TableTd("Show notification bubble")]),
                        dmc.TableTr([dmc.TableTd(dmc.Code("crate_navigate(channel, guild)")), dmc.TableTd("Switch to a channel")]),
                        dmc.TableTr([dmc.TableTd(dmc.Code("crate_send_message(msg)")), dmc.TableTd("Send message on behalf of user")]),
                        dmc.TableTr([dmc.TableTd(dmc.Code("crate_hide() / crate_show()")), dmc.TableTd("Hide/show the button")]),
                        dmc.TableTr([dmc.TableTd(dmc.Code("crate_login() / crate_logout()")), dmc.TableTd("Auth controls")]),
                        dmc.TableTr([dmc.TableTd(dmc.Code("crate_update_options(**opts)")), dmc.TableTd("Update config at runtime")]),
                        dmc.TableTr([dmc.TableTd(dmc.Code("crate_set_color(var, val)")), dmc.TableTd("Set CSS variable")]),
                    ]),
                ],
            ),
        ], gap="sm"),
        withBorder=True, p="lg",
    ),
], gap="md")
```


---

## Slash Commands & Event Bridge



```python
# File: docs/dash_widgetbot/events_example.py

import dash_mantine_components as dmc
from dash_iconify import DashIconify

component = dmc.Stack([
    dmc.Text("Slash Commands & Event Bridge", fw=600, size="lg"),
    dmc.Text(
        "WidgetBot doesn't support native Discord slash commands. dash-widgetbot "
        "implements a message-parsing bridge that intercepts /command text, builds "
        "a fake interaction, and dispatches to registered handlers.",
        size="sm", c="dimmed",
    ),
    dmc.Alert(
        children=dmc.Stack([
            dmc.Text("Try these in the floating Discord chat (bottom-right):", size="sm", fw=500),
            dmc.List([
                dmc.ListItem([dmc.Code("/status"), " — Show app info"]),
                dmc.ListItem([dmc.Code("/ai <prompt>"), " — Generate AI content with Gemini"]),
                dmc.ListItem([dmc.Code("/ask <question>"), " — Ask the AI a question"]),
                dmc.ListItem([dmc.Code("/ai <prompt>"), " + attach an image — Multimodal AI"]),
            ], size="sm"),
        ], gap="xs"),
        title="Live Slash Commands",
        color="indigo",
        icon=DashIconify(icon="tabler:terminal-2", width=20),
    ),
    dmc.Paper(
        dmc.Stack([
            dmc.Text("How the Bridge Works", fw=600, size="sm"),
            dmc.Code(
                """# 1. User types "/ai explain this image" in the Crate
# 2. WidgetBot fires sentMessage via postMessage
# 3. widgetbot_fix.js intercepts and forwards to Dash store:
window.dash_clientside.set_props('_widgetbot-crate-event', {
    data: {type: 'sentMessage', content: '/ai explain...', ...}
})

# 4. Python callback parses the command:
@callback(
    Output("_crate-slash-result", "data"),
    Output("_widgetbot-crate-command", "data", allow_duplicate=True),
    Input("_widgetbot-crate-event", "data"),
    prevent_initial_call=True,
)
def _handle_crate_slash(event_data):
    content = event_data.get("content", "")
    if not content.startswith("/"):
        return no_update, no_update
    # Parse "/ai prompt" → cmd_name="ai", rest="prompt"
    parts = content[1:].split(None, 1)
    cmd_name, rest = parts[0], parts[1] if len(parts) > 1 else ""
    # Build fake interaction and dispatch to handler...

# 5. Handler generates AI response via Gemini
# 6. Response posted to Discord channel via bot token
# 7. Message appears in the Crate widget""",
                block=True,
            ),
        ], gap="sm"),
        withBorder=True, p="lg",
    ),
    dmc.Paper(
        dmc.Stack([
            dmc.Text("Store IDs (Event System)", fw=600, size="sm"),
            dmc.Text(
                "add_discord_crate() returns 6 store IDs for the command/event bridge:",
                size="sm", c="dimmed",
            ),
            dmc.Table(
                [
                    dmc.TableThead(dmc.TableTr([
                        dmc.TableTh("Key"),
                        dmc.TableTh("Store ID"),
                        dmc.TableTh("Direction"),
                        dmc.TableTh("Purpose"),
                    ])),
                    dmc.TableTbody([
                        dmc.TableTr([dmc.TableTd("config"), dmc.TableTd(dmc.Code("_widgetbot-crate-config")), dmc.TableTd("Read"), dmc.TableTd("Initial config (read-only)")]),
                        dmc.TableTr([dmc.TableTd("command"), dmc.TableTd(dmc.Code("_widgetbot-crate-command")), dmc.TableTd("Write"), dmc.TableTd("Send commands to Crate")]),
                        dmc.TableTr([dmc.TableTd("event"), dmc.TableTd(dmc.Code("_widgetbot-crate-event")), dmc.TableTd("Read"), dmc.TableTd("Generic events (sentMessage, etc.)")]),
                        dmc.TableTr([dmc.TableTd("message"), dmc.TableTd(dmc.Code("_widgetbot-crate-message")), dmc.TableTd("Read"), dmc.TableTd("Incoming Discord messages")]),
                        dmc.TableTr([dmc.TableTd("user"), dmc.TableTd(dmc.Code("_widgetbot-crate-user")), dmc.TableTd("Read"), dmc.TableTd("Sign-in / sign-out state")]),
                        dmc.TableTr([dmc.TableTd("status"), dmc.TableTd(dmc.Code("_widgetbot-crate-status")), dmc.TableTd("Read"), dmc.TableTd("Ready + open state")]),
                    ]),
                ],
            ),
        ], gap="sm"),
        withBorder=True, p="lg",
    ),
], gap="md")
```


---

## Setup & Configuration — Development

Baseline initialization order and environment variable contract. Suitable for local dev against a test guild with an ngrok (or cloudflared) tunnel for the interactions endpoint.



```python
# File: docs/dash_widgetbot/setup_guide.py

import dash_mantine_components as dmc
from dash_iconify import DashIconify

component = dmc.Stack([
    dmc.Text("Setup & Configuration", fw=600, size="lg"),
    dmc.Alert(
        children="All hook registrations must happen BEFORE dash.Dash() is created. "
                 "Hooks are processed at app creation time, not at request time.",
        title="Critical: Initialization Order",
        color="red",
        icon=DashIconify(icon="tabler:alert-triangle", width=20),
    ),
    dmc.Paper(
        dmc.Stack([
            dmc.Text("Initialization Order", fw=600, size="sm"),
            dmc.Code(
                """from dotenv import load_dotenv
load_dotenv()

# 1. Register Crate hooks BEFORE Dash()
from dash_widgetbot import add_discord_crate
store_ids = add_discord_crate(
    server=os.getenv("WIDGETBOT_SERVER"),
    channel=os.getenv("WIDGETBOT_CHANNEL"),
    color=os.getenv("WIDGETBOT_COLOR", "#5865f2"),
    shard=os.getenv("WIDGETBOT_SHARD", ""),
)

# 2. Register interactions endpoint BEFORE Dash()
from dash_widgetbot import add_discord_interactions
if os.getenv("DISCORD_PUBLIC_KEY"):
    add_discord_interactions()

# 3. Create Dash app AFTER hooks
import dash
app = dash.Dash(__name__)

# 4. Register command handlers AFTER app
from dash_widgetbot import register_command, sync_discord_endpoint
register_command("ask", my_handler, ephemeral=True)
sync_discord_endpoint()""",
                block=True,
            ),
        ], gap="sm"),
        withBorder=True, p="lg",
    ),
    dmc.Paper(
        dmc.Stack([
            dmc.Text("Environment Variables", fw=600, size="sm"),
            dmc.Table(
                [
                    dmc.TableThead(dmc.TableTr([
                        dmc.TableTh("Variable"),
                        dmc.TableTh("Required"),
                        dmc.TableTh("Description"),
                    ])),
                    dmc.TableTbody([
                        dmc.TableTr([dmc.TableTd(dmc.Code("WIDGETBOT_SERVER")), dmc.TableTd("Yes"), dmc.TableTd("Discord guild snowflake ID")]),
                        dmc.TableTr([dmc.TableTd(dmc.Code("WIDGETBOT_CHANNEL")), dmc.TableTd("Yes"), dmc.TableTd("Default channel snowflake ID")]),
                        dmc.TableTr([dmc.TableTd(dmc.Code("WIDGETBOT_COLOR")), dmc.TableTd("No"), dmc.TableTd("Button hex color (default #5865f2)")]),
                        dmc.TableTr([dmc.TableTd(dmc.Code("WIDGETBOT_SHARD")), dmc.TableTd("No"), dmc.TableTd("Custom WidgetBot shard URL")]),
                        dmc.TableTr([dmc.TableTd(dmc.Code("DISCORD_PUBLIC_KEY")), dmc.TableTd("Bot"), dmc.TableTd("Ed25519 public key for slash commands")]),
                        dmc.TableTr([dmc.TableTd(dmc.Code("DISCORD_APPLICATION_ID")), dmc.TableTd("Bot"), dmc.TableTd("Discord application ID")]),
                        dmc.TableTr([dmc.TableTd(dmc.Code("DISCORD_BOT_TOKEN")), dmc.TableTd("Bot"), dmc.TableTd("Bot token for API calls")]),
                        dmc.TableTr([dmc.TableTd(dmc.Code("DISCORD_WEBHOOK_URL")), dmc.TableTd("Bot"), dmc.TableTd("Webhook URL for sending messages")]),
                        dmc.TableTr([dmc.TableTd(dmc.Code("GEMINI_API_KEY")), dmc.TableTd("AI"), dmc.TableTd("Google Gemini API key for /ai, /ask")]),
                        dmc.TableTr([dmc.TableTd(dmc.Code("GEMINI_MODEL")), dmc.TableTd("No"), dmc.TableTd("Gemini model name (default gemini-2.0-flash)")]),
                    ]),
                ],
            ),
        ], gap="sm"),
        withBorder=True, p="lg",
    ),
    dmc.Paper(
        dmc.Stack([
            dmc.Text("Package Extras", fw=600, size="sm"),
            dmc.Code(
                """pip install dash-widgetbot           # Core (Crate + Widget)
pip install dash-widgetbot[bot]      # + Discord bot (PyNaCl, requests)
pip install dash-widgetbot[ai]       # + Gemini AI (google-genai)
pip install dash-widgetbot[realtime] # + Socket.IO transport
pip install dash-widgetbot[all]      # Everything""",
                block=True,
            ),
        ], gap="sm"),
        withBorder=True, p="lg",
    ),
], gap="md")
```


---

## Production Deployment Guide

Everything a dev run doesn't teach you. The Crate is trivial to ship; the slash-command/bot layer exposes a stack of production-only issues — Cloudflare IP blocks on shared PaaS IPs, `urllib3<2` for HTTPS proxy compatibility, Docker-build vs runtime proxy scope, gunicorn `graceful_timeout` vs daemon-thread Discord follow-up PATCHes, Discord's per-app command-registration rate limit, and the global-vs-guild scope decision.



```python
# File: docs/dash_widgetbot/production_guide.py

"""Production deployment guide for dash-widgetbot.

Captures the lessons learned deploying the Crate + slash-command layers to
a PaaS (Render, Railway, Fly.io) behind Cloudflare-fronted Discord APIs.
Read alongside `setup_guide.py` which covers the baseline env-var contract.
"""

import dash_mantine_components as dmc
from dash_iconify import DashIconify


def _alert(title, text, color, icon):
    return dmc.Alert(
        children=text,
        title=title,
        color=color,
        icon=DashIconify(icon=icon, width=20),
    )


def _paper(title, *children):
    return dmc.Paper(
        dmc.Stack([dmc.Text(title, fw=600, size="sm"), *children], gap="sm"),
        withBorder=True,
        p="lg",
    )


_TWO_LAYERS_TABLE = dmc.Table(
    [
        dmc.TableThead(
            dmc.TableTr(
                [
                    dmc.TableTh(""),
                    dmc.TableTh("Layer 1 — Crate / Widget"),
                    dmc.TableTh("Layer 2 — Slash Commands / Bot"),
                ]
            )
        ),
        dmc.TableTbody(
            [
                dmc.TableTr([dmc.TableTd("Transport"), dmc.TableTd("Client-side iframe"), dmc.TableTd("HTTPS POST from Discord → your server")]),
                dmc.TableTr([dmc.TableTd("Server-side Discord API calls"), dmc.TableTd("None"), dmc.TableTd("Command registration + follow-up PATCH")]),
                dmc.TableTr([dmc.TableTd("Public endpoint required"), dmc.TableTd("No"), dmc.TableTd("Yes (HTTPS, valid cert, publicly reachable)")]),
                dmc.TableTr([dmc.TableTd("Dev vs prod differences"), dmc.TableTd("Identical"), dmc.TableTd("Significant (proxy, gunicorn, registration strategy)")]),
                dmc.TableTr([dmc.TableTd("Required env vars"), dmc.TableTd("WIDGETBOT_SERVER, WIDGETBOT_CHANNEL"), dmc.TableTd("+ DISCORD_PUBLIC_KEY, DISCORD_APPLICATION_ID, DISCORD_BOT_TOKEN")]),
                dmc.TableTr([dmc.TableTd("Extras install"), dmc.TableTd("dash-widgetbot"), dmc.TableTd("dash-widgetbot[bot,ai]")]),
            ]
        ),
    ],
    striped=True,
    withTableBorder=True,
)


_TROUBLESHOOTING_TABLE = dmc.Table(
    [
        dmc.TableThead(
            dmc.TableTr(
                [
                    dmc.TableTh("Symptom"),
                    dmc.TableTh("Likely Cause"),
                    dmc.TableTh("Fix"),
                ]
            )
        ),
        dmc.TableTbody(
            [
                dmc.TableTr(
                    [
                        dmc.TableTd("429 response with Cloudflare HTML from Discord"),
                        dmc.TableTd("PaaS shared IP on Cloudflare blocklist"),
                        dmc.TableTd("Route discord.com through a proxy (QuotaGuard, Fixie, etc.) via HTTP(S)_PROXY + NO_PROXY"),
                    ]
                ),
                dmc.TableTr(
                    [
                        dmc.TableTd("SSL handshake error when proxy is set"),
                        dmc.TableTd("urllib3 v2 changed HTTPS proxy CONNECT behavior"),
                        dmc.TableTd("Pin urllib3<2 in requirements.txt"),
                    ]
                ),
                dmc.TableTr(
                    [
                        dmc.TableTd("Docker build fails reaching pip / npm registries"),
                        dmc.TableTd("Proxy env vars leak into build stage"),
                        dmc.TableTd("Unset HTTP_PROXY / HTTPS_PROXY at the top of the Dockerfile"),
                    ]
                ),
                dmc.TableTr(
                    [
                        dmc.TableTd("Gemini / other APIs slow, timing out, or burning proxy quota"),
                        dmc.TableTd("Global proxy routes non-Discord traffic"),
                        dmc.TableTd("Add those domains to the NO_PROXY list"),
                    ]
                ),
                dmc.TableTr(
                    [
                        dmc.TableTd("Worker killed mid-generation on deploy / restart"),
                        dmc.TableTd("gunicorn graceful_timeout too short for daemon PATCH"),
                        dmc.TableTd("gunicorn.conf.py → graceful_timeout=120, workers=1"),
                    ]
                ),
                dmc.TableTr(
                    [
                        dmc.TableTd("429 on command registration every deploy"),
                        dmc.TableTd("Re-PUTing identical payload on every boot"),
                        dmc.TableTd("Hash the payload, cache the signature, skip PUT when unchanged"),
                    ]
                ),
                dmc.TableTr(
                    [
                        dmc.TableTd("Slash commands appear in one guild but not another"),
                        dmc.TableTd("Guild-scoped registration"),
                        dmc.TableTd("Use the global endpoint /applications/{id}/commands — ~1h propagation"),
                    ]
                ),
                dmc.TableTr(
                    [
                        dmc.TableTd("Command fires but nothing posts back to channel"),
                        dmc.TableTd("Bot missing channel permissions"),
                        dmc.TableTd("Server Settings → Integrations → bot → Channels → grant Send Messages + Embed Links"),
                    ]
                ),
                dmc.TableTr(
                    [
                        dmc.TableTd("Dev Portal rejects Interactions Endpoint URL"),
                        dmc.TableTd("Server not listening, PyNaCl missing, or public key mismatch"),
                        dmc.TableTd("Confirm server is live, pip install dash-widgetbot[bot], DISCORD_PUBLIC_KEY matches the Dev Portal"),
                    ]
                ),
            ]
        ),
    ],
    striped=True,
    withTableBorder=True,
)


component = dmc.Stack(
    [
        dmc.Text("Production Deployment & Troubleshooting", fw=600, size="lg"),
        _alert(
            "Two Independent Layers",
            "The Crate / Widget iframe is a drop-in with no server-side Discord API calls — "
            "it behaves identically in dev and prod. The Bot + slash-command layer is where "
            "production-only issues live: outbound routing, rate limits, proxy config, and "
            "worker-lifecycle timing.",
            "blue",
            "tabler:stack-2",
        ),

        _paper("Layer 1 vs Layer 2", _TWO_LAYERS_TABLE),

        _paper(
            "Development — Slash-command setup",
            dmc.List(
                [
                    dmc.ListItem("Discord Dev Portal → New Application — save Application ID + Public Key"),
                    dmc.ListItem("Bot tab → add bot, generate token (DISCORD_BOT_TOKEN)"),
                    dmc.ListItem("OAuth2 → URL Generator → scopes: applications.commands + bot — invite to your test guild"),
                    dmc.ListItem("Expose localhost publicly: ngrok http 8504 (or cloudflared tunnel)"),
                    dmc.ListItem("Dev Portal → General Information → Interactions Endpoint URL → paste ngrok URL + /api/discord/interactions"),
                    dmc.ListItem("Discord sends a test PING — the save only sticks if Ed25519 verification passes (requires PyNaCl)"),
                    dmc.ListItem("Guild-scoped registration is fine here; it propagates instantly while iterating"),
                ],
                size="sm",
            ),
        ),

        _paper(
            "Production — Outbound proxy for discord.com",
            dmc.Text(
                "Many PaaS hosts (Render, Railway, Heroku free tier, Fly.io edge) share IPs "
                "Cloudflare has blocklisted. Symptoms: 429 responses with a Cloudflare HTML "
                "error page, or a 1020 Access Denied. Route ONLY discord.com through a proxy "
                "(QuotaGuard-style). Everything else goes direct — the NO_PROXY list matters.",
                size="sm",
            ),
            dmc.Code(
                """# run.py — runs BEFORE Dash() so hooks see the env
import os
_proxy_url = os.getenv("QUOTAGUARD_URL")
if _proxy_url:
    os.environ["HTTP_PROXY"]  = _proxy_url
    os.environ["HTTPS_PROXY"] = _proxy_url
    os.environ["NO_PROXY"] = ",".join([
        "generativelanguage.googleapis.com",  # Gemini direct
        "googleapis.com",
        "pypistats.org",
        "pypi.org",
        "github.com",
        "api.github.com",
        "registry.npmjs.org",
        "localhost",
        "127.0.0.1",
    ])""",
                block=True,
            ),
            dmc.Text(
                "Companion fix 1: pin urllib3<2 in requirements.txt — v2 breaks HTTPS proxy SSL handshakes.",
                size="sm",
                c="dimmed",
            ),
            dmc.Text(
                "Companion fix 2: unset HTTP(S)_PROXY at the top of the Dockerfile so pip/npm registries are reachable during build.",
                size="sm",
                c="dimmed",
            ),
            dmc.Code(
                """# Dockerfile (top of file)
ENV HTTP_PROXY=""
ENV HTTPS_PROXY=""
ENV http_proxy=""
ENV https_proxy=""
""",
                block=True,
            ),
        ),

        _paper(
            "Production — Gunicorn tuning for daemon-thread handlers",
            dmc.Text(
                "Slash-command handlers return a type-5 deferred response in <50ms, then run "
                "Gemini generation + Discord follow-up PATCH in a daemon thread (15-45s). "
                "gunicorn's default graceful_timeout=30 kills these mid-PATCH on rolling deploys.",
                size="sm",
            ),
            dmc.Code(
                """# gunicorn.conf.py
workers          = 1       # shared _command_handlers registry stays in one process
worker_class     = "sync"
timeout          = 120     # request timeout
graceful_timeout = 120     # SIGTERM → SIGKILL grace window for in-flight daemons
bind             = "0.0.0.0:8550"
keepalive        = 5
accesslog        = "-"
errorlog         = "-"
""",
                block=True,
            ),
        ),

        _paper(
            "Production — Command registration strategy",
            dmc.List(
                [
                    dmc.ListItem("Use the GLOBAL endpoint PUT /applications/{app_id}/commands — not /guilds/{gid}/commands — so one registration covers every guild the bot joins"),
                    dmc.ListItem("Hash the command payload + app_id, cache the signature on disk (e.g. /tmp/pip_docs_discord_cmds.hash); skip the PUT when unchanged to avoid Discord 429s"),
                    dmc.ListItem("Expose DISCORD_FORCE_COMMAND_SYNC=1 as an escape hatch for the cache when intentionally re-registering"),
                    dmc.ListItem("Global registration propagates to guilds in up to ~1 hour on first publish / changes — plan around that cache window"),
                    dmc.ListItem("Run the PUT once at import time (before the Flask server starts), not inside a request handler"),
                ],
                size="sm",
            ),
        ),

        _paper("Troubleshooting matrix", _TROUBLESHOOTING_TABLE),

        _alert(
            "Sanity-check order for a broken production bot",
            "1. Is /api/discord/interactions reachable over HTTPS?  "
            "2. Does the signature verify (PyNaCl installed, public key matches)?  "
            "3. Are outbound requests to discord.com returning 2xx (proxy + NO_PROXY)?  "
            "4. Are commands registered globally, and is it <1h since the last change?  "
            "5. Does the bot have Send Messages + Embed Links in the target channel?",
            "teal",
            "tabler:checklist",
        ),
    ],
    gap="md",
)
```


---

## Crate Properties — `add_discord_crate()`

| Property | Type | Default | Description |
|:---------|:-----|:--------|:------------|
| `server` | string | Required | Discord server (guild) snowflake ID |
| `channel` | string | `""` | Default channel snowflake ID |
| `color` | string | `"#5865f2"` | Button hex color |
| `location` | list | `None` | `[vertical, horizontal]` position |
| `notifications` | bool | `True` | Show notification bubbles |
| `indicator` | bool | `True` | Show unread dot indicator |
| `timeout` | int | `10000` | ms before notification auto-dismiss |
| `defer` | bool | `False` | Lazy-load Crate |
| `pages` | list | `None` | Route paths for visibility scoping |
| `prefix` | string | `""` | Namespace for multi-instance |
| `shard` | string | `""` | Custom shard URL |

---

## Widget Properties — `discord_widget_container()`

| Property | Type | Default | Description |
|:---------|:-----|:--------|:------------|
| `server` | string | Required | Discord server (guild) snowflake ID |
| `channel` | string | `""` | Channel snowflake ID |
| `width` | string | `"100%"` | CSS width |
| `height` | string | `"600px"` | CSS height |
| `shard` | string | `""` | Custom WidgetBot shard URL |

---

## Contributing

Contributions to dash-widgetbot are welcome! Visit the [GitHub repo](https://github.com/pip-install-python/dash-widgetbot/issues) for issues and feature requests.

## License

MIT License.
