> For the complete documentation index, see [llms.txt](https://docs.drakodevelopment.net/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.drakodevelopment.net/addon-system/api-integration/dashboard-api.md).

# Dashboard API

Extend the DrakoBot dashboard with custom pages, navigation items, and API endpoints.

***

## ✨ Features

* **Custom Pages** — Add your own React pages to the dashboard
* **Navigation Integration** — Seamlessly add items to the sidebar
* **API Routes** — Create custom backend endpoints for your addon
* **Full React Support** — Use hooks, state, and all React features
* **Tailwind CSS** — Style with the dashboard's existing design system
* **Authentication** — Automatic auth handling for protected endpoints
* **Hot Reload** — Changes reflect instantly during development

***

## 🚀 Quick Start

### Folder Structure

```
addons/
└── my-addon/
    ├── my-addon.js          # Main addon file (events, etc.)
    └── dashboard/
        ├── config.js         # Dashboard configuration
        └── pages/
            ├── index.jsx     # Main page component
            └── settings.jsx  # Additional pages
```

### Example: Complete Dashboard Addon

**`addons/my-addon/dashboard/config.js`**

{% code expandable="true" %}

```javascript
module.exports = {
    // Register pages
    pages: [
        {
            path: '/addon/my-addon',
            component: 'index',
            title: 'My Addon'
        },
        {
            path: '/addon/my-addon/settings',
            component: 'settings',
            title: 'My Addon Settings'
        }
    ],
    
    // Add to sidebar navigation
    navItems: [
        {
            name: 'My Addon',
            path: '/addon/my-addon',
            emoji: '🚀',
            permission: null,  // null = any logged in user
            order: 50          // Lower = higher in sidebar
        }
    ],
    
    // Custom API endpoints
    apiRoutes: [
        {
            method: 'get',
            path: '/stats',
            handler: async (req, res) => {
                res.json({
                    message: 'Hello from My Addon!',
                    timestamp: new Date().toISOString()
                });
            }
        },
        {
            method: 'post',
            path: '/save',
            handler: async (req, res) => {
                const { setting } = req.body;
                // Save to database...
                res.json({ success: true, setting });
            }
        }
    ]
};
```

{% endcode %}

**`addons/my-addon/dashboard/pages/index.jsx`**

{% code expandable="true" %}

```jsx
import React, { useState, useEffect } from 'react';
import axios from 'axios';

export default function MyAddonPage() {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        const fetchData = async () => {
            try {
                // API path: /api/addons/{addonname}/{path}
                const response = await axios.get('/api/addons/myaddon/stats');
                setData(response.data);
            } catch (error) {
                console.error('Failed to fetch:', error);
            } finally {
                setLoading(false);
            }
        };
        fetchData();
    }, []);

    return (
        <div className="px-4 lg:px-6 space-y-6">
            <div className="glass-card p-6">
                <h1 className="text-xl font-bold text-foreground mb-4">
                    My Addon Dashboard
                </h1>
                
                {loading ? (
                    <p className="text-muted-foreground">Loading...</p>
                ) : (
                    <div className="glass-subtle rounded-xl p-4">
                        <p className="text-foreground">{data?.message}</p>
                        <p className="text-xs text-muted-foreground mt-2">
                            Last updated: {data?.timestamp}
                        </p>
                    </div>
                )}
            </div>
        </div>
    );
}
```

{% endcode %}

***

## 📄 Configuration Reference

### Pages

Register custom pages in your addon:

```javascript
pages: [
    {
        path: '/addon/my-addon',      // URL path (required)
        component: 'index',            // File in pages/ folder (required)
        title: 'My Addon',             // Page title (optional)
        requiredRoles: null            // Array of role IDs or null (optional)
    }
]
```

| Property        | Type            | Required | Description                              |
| --------------- | --------------- | -------- | ---------------------------------------- |
| `path`          | string          | ✅        | URL path for the page                    |
| `component`     | string          | ✅        | Filename in `pages/` (without extension) |
| `title`         | string          | ❌        | Browser tab title                        |
| `requiredRoles` | string\[]\|null | ❌        | Discord role IDs required to access      |

### Navigation Items

Add items to the dashboard sidebar:

```javascript
navItems: [
    {
        name: 'My Addon',              // Display name (required)
        path: '/addon/my-addon',       // Link destination (required)
        emoji: '🚀',                   // Emoji icon (optional)
        icon: 'faPuzzlePiece',         // FontAwesome icon (optional)
        requiredRoles: null,           // Array of role IDs or null (optional)
        order: 50                      // Sort order (optional)
    }
]
```

| Property        | Type            | Required | Description                           |
| --------------- | --------------- | -------- | ------------------------------------- |
| `name`          | string          | ✅        | Display name in sidebar               |
| `path`          | string          | ✅        | Navigation destination                |
| `emoji`         | string          | ❌        | Emoji to display                      |
| `icon`          | string          | ❌        | FontAwesome icon name                 |
| `requiredRoles` | string\[]\|null | ❌        | Discord role IDs required to see item |
| `order`         | number          | ❌        | Sort priority (lower = higher)        |

### API Routes

Create custom backend endpoints:

```javascript
apiRoutes: [
    {
        method: 'get',                 // HTTP method (required)
        path: '/stats',                // Endpoint path (required)
        requiredRoles: null,           // Array of role IDs or null (optional)
        handler: async (req, res) => { // Handler function (required)
            res.json({ data: 'value' });
        }
    }
]
```

| Property        | Type            | Required | Description                                             |
| --------------- | --------------- | -------- | ------------------------------------------------------- |
| `method`        | string          | ✅        | `get`, `post`, `put`, `delete`, `patch`                 |
| `path`          | string          | ✅        | Endpoint path (prefixed with `/api/addons/{addonname}`) |
| `handler`       | function        | ✅        | Express route handler                                   |
| `middleware`    | array           | ❌        | Additional middleware functions                         |
| `requiredRoles` | string\[]\|null | ❌        | Discord role IDs required to access                     |

***

## 🎨 Styling Guide

### Available CSS Classes

The dashboard uses Tailwind CSS with custom utility classes:

{% code expandable="true" %}

```jsx
// Card styles
<div className="glass-card p-6">         {/* Main card container */}
<div className="glass-subtle p-4">       {/* Subtle inner card */}

// Text colors
<h1 className="text-foreground">         {/* Primary text */}
<p className="text-muted-foreground">    {/* Secondary text */}

// Buttons
<button className="px-4 py-2 rounded-xl bg-primary text-primary-foreground">
    Primary Button
</button>

// Inputs
<input className="w-full h-10 px-4 rounded-xl glass-input text-sm" />

// Gradients
<div className="bg-gradient-to-br from-blue-500 to-blue-600">
```

{% endcode %}

### Responsive Design

```jsx
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
    {/* Cards auto-adjust based on screen size */}
</div>
```

***

## 🔌 API Endpoint Patterns

### Accessing Your API

Your API routes are available at:

```
/api/addons/{addonname-lowercase}/{path}
```

**Example:**

* Addon name: `MyAddon`
* Route path: `/stats`
* Full URL: `/api/addons/myaddon/stats`

### Making API Calls

{% code expandable="true" %}

```jsx
import axios from 'axios';

// GET request
const response = await axios.get('/api/addons/myaddon/stats');

// POST request
const response = await axios.post('/api/addons/myaddon/save', {
    setting: 'value'
});

// With error handling
try {
    const { data } = await axios.get('/api/addons/myaddon/stats');
    console.log(data);
} catch (error) {
    if (error.response?.status === 401) {
        console.log('Not authenticated');
    }
}
```

{% endcode %}

### Handler Examples

{% code expandable="true" %}

```javascript
apiRoutes: [
    // Simple GET
    {
        method: 'get',
        path: '/info',
        handler: async (req, res) => {
            res.json({ version: '1.0.0' });
        }
    },
    
    // With request body
    {
        method: 'post',
        path: '/settings',
        handler: async (req, res) => {
            const { enabled, message } = req.body;
            // Validate
            if (typeof enabled !== 'boolean') {
                return res.status(400).json({ error: 'Invalid enabled value' });
            }
            // Save and respond
            res.json({ success: true, enabled, message });
        }
    },
    
    // With URL parameters
    {
        method: 'get',
        path: '/user/:userId',
        handler: async (req, res) => {
            const { userId } = req.params;
            // Fetch user data...
            res.json({ userId, data: {} });
        }
    },
    
    // Database integration
    {
        method: 'get',
        path: '/guild-stats',
        handler: async (req, res) => {
            const mongoose = require('mongoose');
            const stats = await mongoose.model('GuildStats').find({});
            res.json({ stats });
        }
    }
]
```

{% endcode %}

***

## 🔐 Authentication & Permissions

### Using requiredRoles

The `requiredRoles` property accepts an array of Discord role IDs. Users must have **at least one** of the specified roles to access the resource.

```javascript
// Anyone logged in (no role restriction)
requiredRoles: null

// Require specific role(s)
requiredRoles: ['123456789012345678']

// Require any of multiple roles (OR logic)
requiredRoles: ['ROLE_ID1', 'ROLE_ID2', 'ROLE_ID3']
```

### Complete Example with Permissions

{% code expandable="true" %}

```javascript
module.exports = {
    pages: [
        {
            path: '/addon/admin-panel',
            component: 'admin',
            title: 'Admin Panel',
            // Only users with Admin or Owner role can access
            requiredRoles: ['111111111111111111', '222222222222222222']
        },
        {
            path: '/addon/admin-panel/public',
            component: 'public',
            title: 'Public Info',
            // Anyone logged in can access
            requiredRoles: null
        }
    ],
    
    navItems: [
        {
            name: 'Admin Panel',
            path: '/addon/admin-panel',
            emoji: '⚙️',
            // Only show in sidebar for admins
            requiredRoles: ['111111111111111111', '222222222222222222']
        }
    ],
    
    apiRoutes: [
        {
            method: 'get',
            path: '/public-data',
            requiredRoles: null,  // Public endpoint
            handler: async (req, res) => {
                res.json({ public: true });
            }
        },
        {
            method: 'post',
            path: '/admin-action',
            requiredRoles: ['111111111111111111'],  // Admin only
            handler: async (req, res) => {
                // Only admins reach here
                res.json({ success: true });
            }
        }
    ]
};
```

{% endcode %}

### Getting Role IDs

{% stepper %}
{% step %}
Enable Developer Mode in Discord:

* Settings → Advanced → Developer Mode
  {% endstep %}

{% step %}
Copy the role ID:

* Right-click the role → Copy Role ID
  {% endstep %}
  {% endstepper %}

### Checking User in API Handler

{% code expandable="true" %}

```javascript
{
    method: 'get',
    path: '/user-info',
    handler: async (req, res) => {
        // req.user is available after auth
        const user = req.user || req.session?.userData;
        
        if (!user) {
            return res.status(401).json({ error: 'Not authenticated' });
        }
        
        // Access user data
        res.json({ 
            userId: user.id,
            username: user.username,
            roles: user.roles  // Array of role IDs
        });
    }
}
```

{% endcode %}

***

## 🧩 Complete Examples

### Stats Dashboard

{% code expandable="true" %}

```javascript
// config.js
module.exports = {
    pages: [
        { path: '/addon/stats', component: 'index', title: 'Server Stats' }
    ],
    navItems: [
        { name: 'Stats', path: '/addon/stats', emoji: '📊', order: 10 }
    ],
    apiRoutes: [
        {
            method: 'get',
            path: '/overview',
            handler: async (req, res) => {
                const client = require('../../bot').getDiscordClient();
                
                res.json({
                    guilds: client.guilds.cache.size,
                    users: client.users.cache.size,
                    channels: client.channels.cache.size,
                    uptime: process.uptime()
                });
            }
        }
    ]
};
```

{% endcode %}

{% code expandable="true" %}

```jsx
// pages/index.jsx
import React, { useState, useEffect } from 'react';
import axios from 'axios';

export default function StatsPage() {
    const [stats, setStats] = useState(null);

    useEffect(() => {
        axios.get('/api/addons/stats/overview')
            .then(res => setStats(res.data))
            .catch(console.error);
    }, []);

    if (!stats) return <div className="text-muted-foreground">Loading...</div>;

    return (
        <div className="px-4 lg:px-6">
            <h1 className="text-2xl font-bold text-foreground mb-6">Server Stats</h1>
            
            <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
                <StatCard label="Guilds" value={stats.guilds} emoji="🏠" />
                <StatCard label="Users" value={stats.users} emoji="👥" />
                <StatCard label="Channels" value={stats.channels} emoji="📺" />
                <StatCard label="Uptime" value={formatUptime(stats.uptime)} emoji="⏱️" />
            </div>
        </div>
    );
}

function StatCard({ label, value, emoji }) {
    return (
        <div className="glass-card p-4">
            <div className="text-2xl mb-2">{emoji}</div>
            <div className="text-2xl font-bold text-foreground">{value}</div>
            <div className="text-sm text-muted-foreground">{label}</div>
        </div>
    );
}

function formatUptime(seconds) {
    const hours = Math.floor(seconds / 3600);
    const minutes = Math.floor((seconds % 3600) / 60);
    return `${hours}h ${minutes}m`;
}
```

{% endcode %}

### Settings Panel

{% code expandable="true" %}

```javascript
// config.js
module.exports = {
    pages: [
        { path: '/addon/my-settings', component: 'index', title: 'My Settings' }
    ],
    navItems: [
        { name: 'My Settings', path: '/addon/my-settings', emoji: '⚙️' }
    ],
    apiRoutes: [
        {
            method: 'get',
            path: '/config',
            handler: async (req, res) => {
                // Load from database or file
                res.json({
                    enabled: true,
                    welcomeMessage: 'Welcome!',
                    notifyChannel: '123456789'
                });
            }
        },
        {
            method: 'post',
            path: '/config',
            handler: async (req, res) => {
                const { enabled, welcomeMessage, notifyChannel } = req.body;
                // Save to database...
                res.json({ success: true });
            }
        }
    ]
};
```

{% endcode %}

{% code expandable="true" %}

```jsx
// pages/index.jsx
import React, { useState, useEffect } from 'react';
import axios from 'axios';

export default function SettingsPage() {
    const [settings, setSettings] = useState({
        enabled: false,
        welcomeMessage: '',
        notifyChannel: ''
    });
    const [saving, setSaving] = useState(false);

    useEffect(() => {
        axios.get('/api/addons/my-settings/config')
            .then(res => setSettings(res.data))
            .catch(console.error);
    }, []);

    const handleSave = async () => {
        setSaving(true);
        try {
            await axios.post('/api/addons/my-settings/config', settings);
            alert('Settings saved!');
        } catch (error) {
            alert('Failed to save');
        } finally {
            setSaving(false);
        }
    };

    return (
        <div className="px-4 lg:px-6 space-y-6">
            <div className="glass-card p-6">
                <h1 className="text-xl font-bold text-foreground mb-6">Settings</h1>
                
                {/* Toggle */}
                <div className="flex items-center justify-between p-4 glass-subtle rounded-xl mb-4">
                    <div>
                        <h3 className="text-sm font-medium text-foreground">Enable Feature</h3>
                        <p className="text-xs text-muted-foreground">Toggle the feature on/off</p>
                    </div>
                    <button
                        onClick={() => setSettings(s => ({ ...s, enabled: !s.enabled }))}
                        className={`w-12 h-6 rounded-full transition-colors ${
                            settings.enabled ? 'bg-primary' : 'bg-secondary'
                        }`}
                    >
                        <span className={`block w-4 h-4 rounded-full bg-white transition-transform ${
                            settings.enabled ? 'translate-x-7' : 'translate-x-1'
                        }`} />
                    </button>
                </div>
                
                {/* Text Input */}
                <div className="p-4 glass-subtle rounded-xl mb-4">
                    <label className="text-sm font-medium text-foreground block mb-2">
                        Welcome Message
                    </label>
                    <input
                        type="text"
                        value={settings.welcomeMessage}
                        onChange={e => setSettings(s => ({ ...s, welcomeMessage: e.target.value }))}
                        className="w-full h-10 px-4 rounded-xl glass-input text-sm"
                        placeholder="Enter message..."
                    />
                </div>
                
                {/* Save Button */}
                <div className="flex justify-end">
                    <button
                        onClick={handleSave}
                        disabled={saving}
                        className="px-6 py-2.5 rounded-xl bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 transition-colors disabled:opacity-50"
                    >
                        {saving ? 'Saving...' : 'Save Settings'}
                    </button>
                </div>
            </div>
        </div>
    );
}
```

{% endcode %}

***

## 📋 Requirements Summary

### Minimum Required

```
addons/my-addon/
└── dashboard/
    ├── config.js      # Must export { pages, navItems, apiRoutes }
    └── pages/
        └── index.jsx  # At least one page component
```

### config.js Template

```javascript
module.exports = {
    pages: [],      // Required (can be empty)
    navItems: [],   // Required (can be empty)
    apiRoutes: []   // Required (can be empty)
};
```

### Page Component Template

```jsx
import React from 'react';

export default function MyPage() {
    return (
        <div className="px-4 lg:px-6">
            <div className="glass-card p-6">
                <h1 className="text-xl font-bold text-foreground">My Page</h1>
            </div>
        </div>
    );
}
```

***

## 🔄 Integration with Event API

Combine dashboard with event handlers:

**`addons/my-addon/my-addon.js`**

{% code expandable="true" %}

```javascript
module.exports = {
    name: 'MyAddon',
    version: '1.0.0',
    
    // Event handlers
    events: {
        'discord:messageCreate': async (eventData, context) => {
            // Handle Discord events
        }
    },
    
    // Initialization
    run: async (client, eventManager) => {
        console.log('[MyAddon] Loaded with dashboard support!');
    }
};
```

{% endcode %}

The dashboard config is automatically loaded from `dashboard/config.js` when the addon initializes.

***

## 💡 Tips & Best Practices

{% stepper %}
{% step %}
Use meaningful addon names — The addon name becomes part of the API URL
{% endstep %}

{% step %}
Handle loading states — Always show loading indicators
{% endstep %}

{% step %}
Error handling — Catch and display errors gracefully
{% endstep %}

{% step %}
API validation — Always validate request data on the backend
{% endstep %}

{% step %}
Permission checks — Use permissions for sensitive features
{% endstep %}
{% endstepper %}

***

## Troubleshooting

<details>

<summary>Page not loading</summary>

* Check browser console for errors
* Verify file exists in `dashboard/pages/`
* Ensure component has `export default`

</details>

<details>

<summary>API returns 404</summary>

* Check addon name is lowercase in URL
* Verify route is registered in config.js
* Check server logs for registration messages

</details>

<details>

<summary>Styles not applying</summary>

* Use existing Tailwind classes
* Check class names are correct
* Verify you're using the right CSS variable names

</details>

<details>

<summary>Authentication issues</summary>

* Ensure you're logged into the dashboard
* Check if route requires specific permissions
* Verify session is active

</details>

***


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.drakodevelopment.net/addon-system/api-integration/dashboard-api.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
