migrated core_nest to mainroom
This commit is contained in:
311
mainroom/sbwrapper/README.md
Executable file
311
mainroom/sbwrapper/README.md
Executable file
@@ -0,0 +1,311 @@
|
||||
# Pawprint Wrapper - Development Tools Sidebar
|
||||
|
||||
A collapsible sidebar that provides development and testing tools for any pawprint-managed nest (like amar) without interfering with the managed application.
|
||||
|
||||
## Features
|
||||
|
||||
### 👤 Quick Login
|
||||
- Switch between test users with one click
|
||||
- Pre-configured admin, vet, and tutor accounts
|
||||
- Automatic JWT token management
|
||||
- Shows currently logged-in user
|
||||
|
||||
### 🌍 Environment Info
|
||||
- Display backend and frontend URLs
|
||||
- Nest name and deployment info
|
||||
- Quick reference during development
|
||||
|
||||
### ⌨️ Keyboard Shortcuts
|
||||
- **Ctrl+Shift+P** - Toggle sidebar
|
||||
|
||||
### 💾 State Persistence
|
||||
- Sidebar remembers expanded/collapsed state
|
||||
- Persists across page reloads
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
wrapper/
|
||||
├── index.html # Standalone demo
|
||||
├── sidebar.css # Sidebar styling
|
||||
├── sidebar.js # Sidebar logic
|
||||
├── config.json # Configuration (users, URLs)
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Standalone Demo
|
||||
|
||||
Open `index.html` in your browser to see the sidebar in action:
|
||||
|
||||
```bash
|
||||
cd core_nest/wrapper
|
||||
python3 -m http.server 8080
|
||||
# Open http://localhost:8080
|
||||
```
|
||||
|
||||
Click the toggle button on the right edge or press **Ctrl+Shift+P**.
|
||||
|
||||
### Integration with Your App
|
||||
|
||||
Add these two lines to your HTML:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="/wrapper/sidebar.css">
|
||||
<script src="/wrapper/sidebar.js"></script>
|
||||
```
|
||||
|
||||
The sidebar will automatically:
|
||||
1. Load configuration from `/wrapper/config.json`
|
||||
2. Create the sidebar UI
|
||||
3. Setup keyboard shortcuts
|
||||
4. Check for existing logged-in users
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `config.json` to customize:
|
||||
|
||||
```json
|
||||
{
|
||||
"nest_name": "amar",
|
||||
"wrapper": {
|
||||
"enabled": true,
|
||||
"environment": {
|
||||
"backend_url": "http://localhost:8000",
|
||||
"frontend_url": "http://localhost:3000"
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"id": "admin",
|
||||
"label": "Admin",
|
||||
"username": "admin@test.com",
|
||||
"password": "Amar2025!",
|
||||
"icon": "👑",
|
||||
"role": "ADMIN"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### User Fields
|
||||
|
||||
- **id**: Unique identifier for the user
|
||||
- **label**: Display name in the sidebar
|
||||
- **username**: Login username (email)
|
||||
- **password**: Login password
|
||||
- **icon**: Emoji icon to display
|
||||
- **role**: User role (ADMIN, VET, USER)
|
||||
|
||||
## How It Works
|
||||
|
||||
### Login Flow
|
||||
|
||||
1. User clicks a user card in the sidebar
|
||||
2. `sidebar.js` calls `POST {backend_url}/api/token/` with credentials
|
||||
3. Backend returns JWT tokens: `{ access, refresh, details }`
|
||||
4. Tokens stored in localStorage
|
||||
5. Page reloads, user is now logged in
|
||||
|
||||
### Token Storage
|
||||
|
||||
Tokens are stored in localStorage:
|
||||
- `access_token` - JWT access token
|
||||
- `refresh_token` - JWT refresh token
|
||||
- `user_info` - User metadata (username, label, role)
|
||||
|
||||
### Logout Flow
|
||||
|
||||
1. User clicks "Logout" button
|
||||
2. Tokens removed from localStorage
|
||||
3. Page reloads, user is logged out
|
||||
|
||||
## Docker Integration
|
||||
|
||||
### Approach 1: Static Files
|
||||
|
||||
Mount wrapper as static files in docker-compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
frontend:
|
||||
volumes:
|
||||
- ./ctrl/wrapper:/app/public/wrapper:ro
|
||||
```
|
||||
|
||||
Then in your HTML:
|
||||
```html
|
||||
<link rel="stylesheet" href="/wrapper/sidebar.css">
|
||||
<script src="/wrapper/sidebar.js"></script>
|
||||
```
|
||||
|
||||
### Approach 2: Nginx Injection
|
||||
|
||||
Use nginx to inject the sidebar script automatically:
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
sub_filter '</head>' '<link rel="stylesheet" href="/wrapper/sidebar.css"><script src="/wrapper/sidebar.js"></script></head>';
|
||||
sub_filter_once on;
|
||||
proxy_pass http://frontend:3000;
|
||||
}
|
||||
|
||||
location /wrapper/ {
|
||||
alias /app/wrapper/;
|
||||
}
|
||||
```
|
||||
|
||||
### Approach 3: Wrapper Service
|
||||
|
||||
Create a dedicated wrapper service:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
wrapper:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./ctrl/wrapper:/usr/share/nginx/html/wrapper
|
||||
environment:
|
||||
- MANAGED_APP_URL=http://frontend:3000
|
||||
```
|
||||
|
||||
See `../WRAPPER_DESIGN.md` for detailed Docker integration patterns.
|
||||
|
||||
## Customization
|
||||
|
||||
### Styling
|
||||
|
||||
Edit `sidebar.css` to customize appearance:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--sidebar-width: 320px;
|
||||
--sidebar-bg: #1e1e1e;
|
||||
--sidebar-text: #e0e0e0;
|
||||
--sidebar-accent: #007acc;
|
||||
}
|
||||
```
|
||||
|
||||
### Add New Panels
|
||||
|
||||
Add HTML to `getSidebarHTML()` in `sidebar.js`:
|
||||
|
||||
```javascript
|
||||
getSidebarHTML() {
|
||||
return `
|
||||
...existing panels...
|
||||
|
||||
<div class="panel">
|
||||
<h3>🆕 My New Panel</h3>
|
||||
<p>Custom content here</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
### Add New Features
|
||||
|
||||
Extend the `PawprintSidebar` class in `sidebar.js`:
|
||||
|
||||
```javascript
|
||||
class PawprintSidebar {
|
||||
async fetchJiraInfo() {
|
||||
const response = await fetch('https://artery.mcrn.ar/jira/VET-123');
|
||||
const data = await response.json();
|
||||
// Update UI with data
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Requirements
|
||||
|
||||
The sidebar expects these endpoints from your backend:
|
||||
|
||||
### POST /api/token/
|
||||
Login endpoint that returns JWT tokens.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"username": "admin@test.com",
|
||||
"password": "Amar2025!"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"access": "eyJ0eXAiOiJKV1QiLCJhbGc...",
|
||||
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGc...",
|
||||
"details": {
|
||||
"role": "ADMIN",
|
||||
"id": 1,
|
||||
"name": "Admin User"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Sidebar not appearing
|
||||
- Check browser console for errors
|
||||
- Verify `sidebar.js` and `sidebar.css` are loaded
|
||||
- Check that `config.json` is accessible
|
||||
|
||||
### Login fails
|
||||
- Verify backend URL in `config.json`
|
||||
- Check backend is running
|
||||
- Verify credentials are correct
|
||||
- Check CORS settings on backend
|
||||
|
||||
### Tokens not persisting
|
||||
- Check localStorage is enabled
|
||||
- Verify domain matches between sidebar and app
|
||||
- Check browser privacy settings
|
||||
|
||||
## Security Considerations
|
||||
|
||||
⚠️ **Important:** This sidebar is for **development/testing only**.
|
||||
|
||||
- Passwords are stored in plain text in `config.json`
|
||||
- Do NOT use in production
|
||||
- Do NOT commit real credentials to git
|
||||
- Add `config.json` to `.gitignore` if it contains sensitive data
|
||||
|
||||
For production:
|
||||
- Disable wrapper via `"enabled": false` in config
|
||||
- Use environment variables for URLs
|
||||
- Remove or secure test user credentials
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Planned features (see `../WRAPPER_DESIGN.md`):
|
||||
|
||||
- 📋 **Jira Info Panel** - Fetch ticket details from artery
|
||||
- 📊 **Logs Viewer** - Stream container logs
|
||||
- 🎨 **Theme Switcher** - Light/dark mode
|
||||
- 🔍 **Search** - Quick search across tools
|
||||
- ⚙️ **Settings** - Customize sidebar behavior
|
||||
- 📱 **Mobile Support** - Responsive design improvements
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- `../WRAPPER_DESIGN.md` - Complete architecture design
|
||||
- `../../../pawprint/CLAUDE.md` - Pawprint framework overview
|
||||
- `../../README.md` - Core nest documentation
|
||||
|
||||
## Contributing
|
||||
|
||||
To add a new panel or feature:
|
||||
|
||||
1. Add HTML in `getSidebarHTML()`
|
||||
2. Add styling in `sidebar.css`
|
||||
3. Add logic as methods on `PawprintSidebar` class
|
||||
4. Update this README with usage instructions
|
||||
|
||||
## License
|
||||
|
||||
Part of the Pawprint development tools ecosystem.
|
||||
40
mainroom/sbwrapper/config.json
Executable file
40
mainroom/sbwrapper/config.json
Executable file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"nest_name": "amar",
|
||||
"wrapper": {
|
||||
"enabled": true,
|
||||
"environment": {
|
||||
"backend_url": "http://localhost:8000",
|
||||
"frontend_url": "http://localhost:3000"
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"id": "admin",
|
||||
"label": "Admin",
|
||||
"username": "admin@test.com",
|
||||
"password": "Amar2025!",
|
||||
"icon": "👑",
|
||||
"role": "ADMIN"
|
||||
},
|
||||
{
|
||||
"id": "vet1",
|
||||
"label": "Vet 1",
|
||||
"username": "vet@test.com",
|
||||
"password": "Amar2025!",
|
||||
"icon": "🩺",
|
||||
"role": "VET"
|
||||
},
|
||||
{
|
||||
"id": "tutor1",
|
||||
"label": "Tutor 1",
|
||||
"username": "tutor@test.com",
|
||||
"password": "Amar2025!",
|
||||
"icon": "🐶",
|
||||
"role": "USER"
|
||||
}
|
||||
],
|
||||
"jira": {
|
||||
"ticket_id": "VET-535",
|
||||
"epic": "EPIC-51.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
197
mainroom/sbwrapper/index.html
Executable file
197
mainroom/sbwrapper/index.html
Executable file
@@ -0,0 +1,197 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pawprint Wrapper - Demo</title>
|
||||
<link rel="stylesheet" href="sidebar.css">
|
||||
<style>
|
||||
/* Demo page styles */
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
#demo-content {
|
||||
padding: 40px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
transition: margin-right 0.3s ease;
|
||||
}
|
||||
|
||||
#pawprint-sidebar.expanded ~ #demo-content {
|
||||
margin-right: var(--sidebar-width);
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.demo-header h1 {
|
||||
font-size: 32px;
|
||||
margin-bottom: 8px;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.demo-header p {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 32px;
|
||||
padding: 24px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #007acc;
|
||||
}
|
||||
|
||||
.demo-section h2 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 16px;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.demo-section p {
|
||||
color: #444;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.demo-section code {
|
||||
background: #e0e0e0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.demo-section ul {
|
||||
margin-left: 20px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.demo-section li {
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.status-box {
|
||||
padding: 16px;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.status-box strong {
|
||||
color: #007acc;
|
||||
}
|
||||
|
||||
.kbd {
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 0 #bbb;
|
||||
font-family: 'Monaco', monospace;
|
||||
font-size: 12px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="demo-content">
|
||||
<div class="demo-header">
|
||||
<h1>🐾 Pawprint Wrapper</h1>
|
||||
<p>Development tools sidebar for any pawprint-managed nest</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2>👋 Quick Start</h2>
|
||||
<p>
|
||||
This is a standalone demo of the Pawprint Wrapper sidebar.
|
||||
Click the toggle button on the right edge of the screen, or press
|
||||
<span class="kbd">Ctrl</span> + <span class="kbd">Shift</span> + <span class="kbd">P</span>
|
||||
to open the sidebar.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2>🎯 Features</h2>
|
||||
<ul>
|
||||
<li><strong>Quick Login:</strong> Switch between test users with one click</li>
|
||||
<li><strong>Environment Info:</strong> See current backend/frontend URLs</li>
|
||||
<li><strong>JWT Token Management:</strong> Automatic token storage and refresh</li>
|
||||
<li><strong>Keyboard Shortcuts:</strong> Ctrl+Shift+P to toggle</li>
|
||||
<li><strong>Persistent State:</strong> Sidebar remembers expanded/collapsed state</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2>👤 Test Users</h2>
|
||||
<p>Try logging in as one of these test users (from <code>config.json</code>):</p>
|
||||
<ul>
|
||||
<li>👑 <strong>Admin</strong> - admin@test.com / Amar2025!</li>
|
||||
<li>🩺 <strong>Vet 1</strong> - vet@test.com / Amar2025!</li>
|
||||
<li>🐶 <strong>Tutor 1</strong> - tutor@test.com / Amar2025!</li>
|
||||
</ul>
|
||||
|
||||
<div class="status-box">
|
||||
<strong>Note:</strong> In this demo, login will fail because there's no backend running.
|
||||
When integrated with a real AMAR instance, clicking a user card will:
|
||||
<ol style="margin-top: 8px; margin-left: 20px;">
|
||||
<li>Call <code>POST /api/token/</code> with username/password</li>
|
||||
<li>Store access & refresh tokens in localStorage</li>
|
||||
<li>Reload the page with the user logged in</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2>🔧 How It Works</h2>
|
||||
<p>The sidebar is implemented as three files:</p>
|
||||
<ul>
|
||||
<li><code>sidebar.css</code> - Visual styling (dark theme, animations)</li>
|
||||
<li><code>sidebar.js</code> - Logic (login, logout, toggle, state management)</li>
|
||||
<li><code>config.json</code> - Configuration (users, URLs, nest info)</li>
|
||||
</ul>
|
||||
<p style="margin-top: 16px;">
|
||||
To integrate with your app, simply include these in your HTML:
|
||||
</p>
|
||||
<div style="background: #fff; padding: 12px; border-radius: 4px; margin-top: 8px;">
|
||||
<code style="display: block; font-size: 13px;">
|
||||
<link rel="stylesheet" href="/wrapper/sidebar.css"><br>
|
||||
<script src="/wrapper/sidebar.js"></script>
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2>🚀 Next Steps</h2>
|
||||
<p>Planned enhancements:</p>
|
||||
<ul>
|
||||
<li>📋 <strong>Jira Info Panel:</strong> Fetch and display ticket details from artery</li>
|
||||
<li>📊 <strong>Logs Viewer:</strong> Stream container logs via WebSocket</li>
|
||||
<li>🎨 <strong>Theme Switcher:</strong> Light/dark theme toggle</li>
|
||||
<li>🔍 <strong>Search:</strong> Quick search across users and tools</li>
|
||||
<li>⚙️ <strong>Settings:</strong> Customize sidebar behavior</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2>📚 Documentation</h2>
|
||||
<p>
|
||||
See <code>WRAPPER_DESIGN.md</code> in <code>core_nest/</code> for the complete
|
||||
architecture design, including Docker integration patterns and alternative approaches.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Load the sidebar -->
|
||||
<script src="sidebar.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
296
mainroom/sbwrapper/sidebar.css
Executable file
296
mainroom/sbwrapper/sidebar.css
Executable file
@@ -0,0 +1,296 @@
|
||||
/* Pawprint Wrapper - Sidebar Styles */
|
||||
|
||||
:root {
|
||||
--sidebar-width: 320px;
|
||||
--sidebar-bg: #1e1e1e;
|
||||
--sidebar-text: #e0e0e0;
|
||||
--sidebar-accent: #007acc;
|
||||
--sidebar-border: #333;
|
||||
--sidebar-shadow: 0 0 20px rgba(0,0,0,0.5);
|
||||
--card-bg: #2a2a2a;
|
||||
--card-hover: #3a3a3a;
|
||||
--success: #4caf50;
|
||||
--error: #f44336;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Sidebar Container */
|
||||
#pawprint-sidebar {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: var(--sidebar-width);
|
||||
height: 100vh;
|
||||
background: var(--sidebar-bg);
|
||||
color: var(--sidebar-text);
|
||||
box-shadow: var(--sidebar-shadow);
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 9999;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#pawprint-sidebar.expanded {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* Toggle Button */
|
||||
#sidebar-toggle {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: var(--sidebar-bg);
|
||||
color: var(--sidebar-text);
|
||||
border: 1px solid var(--sidebar-border);
|
||||
border-right: none;
|
||||
border-radius: 8px 0 0 8px;
|
||||
padding: 12px 8px;
|
||||
cursor: pointer;
|
||||
z-index: 10000;
|
||||
font-size: 16px;
|
||||
transition: background 0.2s;
|
||||
box-shadow: -2px 0 8px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
#sidebar-toggle:hover {
|
||||
background: var(--card-hover);
|
||||
}
|
||||
|
||||
#sidebar-toggle .icon {
|
||||
display: block;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
#pawprint-sidebar.expanded ~ #sidebar-toggle .icon {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.sidebar-header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid var(--sidebar-border);
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%);
|
||||
}
|
||||
|
||||
.sidebar-header h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
color: var(--sidebar-accent);
|
||||
}
|
||||
|
||||
.sidebar-header .nest-name {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.sidebar-content {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Panel */
|
||||
.panel {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: var(--card-bg);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--sidebar-border);
|
||||
}
|
||||
|
||||
.panel h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
color: var(--sidebar-accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Current User Display */
|
||||
.current-user {
|
||||
padding: 12px;
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.current-user strong {
|
||||
color: var(--success);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.current-user .logout-btn {
|
||||
margin-top: 8px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||
color: var(--error);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.current-user .logout-btn:hover {
|
||||
background: rgba(244, 67, 54, 0.2);
|
||||
}
|
||||
|
||||
/* User Cards */
|
||||
.user-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--sidebar-border);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.user-card:hover {
|
||||
background: var(--card-hover);
|
||||
border-color: var(--sidebar-accent);
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
|
||||
.user-card.active {
|
||||
background: rgba(0, 122, 204, 0.2);
|
||||
border-color: var(--sidebar-accent);
|
||||
}
|
||||
|
||||
.user-card .icon {
|
||||
font-size: 24px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.user-card .info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-card .label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.user-card .role {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
opacity: 0.6;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Status Messages */
|
||||
.status-message {
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.status-message.success {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
border-color: rgba(76, 175, 80, 0.3);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.status-message.error {
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
border-color: rgba(244, 67, 54, 0.3);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.status-message.info {
|
||||
background: rgba(0, 122, 204, 0.1);
|
||||
border-color: rgba(0, 122, 204, 0.3);
|
||||
color: var(--sidebar-accent);
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid rgba(255,255,255,0.1);
|
||||
border-top-color: var(--sidebar-accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
#pawprint-sidebar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
#pawprint-sidebar::-webkit-scrollbar-track {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
#pawprint-sidebar::-webkit-scrollbar-thumb {
|
||||
background: #444;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#pawprint-sidebar::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.sidebar-footer {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid var(--sidebar-border);
|
||||
font-size: 11px;
|
||||
opacity: 0.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
#pawprint-sidebar {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
286
mainroom/sbwrapper/sidebar.js
Executable file
286
mainroom/sbwrapper/sidebar.js
Executable file
@@ -0,0 +1,286 @@
|
||||
// Pawprint Wrapper - Sidebar Logic
|
||||
|
||||
class PawprintSidebar {
|
||||
constructor() {
|
||||
this.config = null;
|
||||
this.currentUser = null;
|
||||
this.sidebar = null;
|
||||
this.toggleBtn = null;
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Load configuration
|
||||
await this.loadConfig();
|
||||
|
||||
// Create sidebar elements
|
||||
this.createSidebar();
|
||||
this.createToggleButton();
|
||||
|
||||
// Setup event listeners
|
||||
this.setupEventListeners();
|
||||
|
||||
// Check if user is already logged in
|
||||
this.checkCurrentUser();
|
||||
|
||||
// Load saved sidebar state
|
||||
this.loadSidebarState();
|
||||
}
|
||||
|
||||
async loadConfig() {
|
||||
try {
|
||||
const response = await fetch('/wrapper/config.json');
|
||||
this.config = await response.json();
|
||||
console.log('[Pawprint] Config loaded:', this.config.nest_name);
|
||||
} catch (error) {
|
||||
console.error('[Pawprint] Failed to load config:', error);
|
||||
// Use default config
|
||||
this.config = {
|
||||
nest_name: 'default',
|
||||
wrapper: {
|
||||
environment: {
|
||||
backend_url: 'http://localhost:8000',
|
||||
frontend_url: 'http://localhost:3000'
|
||||
},
|
||||
users: []
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
createSidebar() {
|
||||
const sidebar = document.createElement('div');
|
||||
sidebar.id = 'pawprint-sidebar';
|
||||
sidebar.innerHTML = this.getSidebarHTML();
|
||||
document.body.appendChild(sidebar);
|
||||
this.sidebar = sidebar;
|
||||
}
|
||||
|
||||
createToggleButton() {
|
||||
const button = document.createElement('button');
|
||||
button.id = 'sidebar-toggle';
|
||||
button.innerHTML = '<span class="icon">◀</span>';
|
||||
button.title = 'Toggle Pawprint Sidebar (Ctrl+Shift+P)';
|
||||
document.body.appendChild(button);
|
||||
this.toggleBtn = button;
|
||||
}
|
||||
|
||||
getSidebarHTML() {
|
||||
const users = this.config.wrapper.users || [];
|
||||
|
||||
return `
|
||||
<div class="sidebar-header">
|
||||
<h2>🐾 Pawprint</h2>
|
||||
<div class="nest-name">${this.config.nest_name}</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-content">
|
||||
<div id="status-container"></div>
|
||||
|
||||
<!-- Quick Login Panel -->
|
||||
<div class="panel">
|
||||
<h3>👤 Quick Login</h3>
|
||||
|
||||
<div id="current-user-display" style="display: none;">
|
||||
<div class="current-user">
|
||||
Logged in as: <strong id="current-username"></strong>
|
||||
<button class="logout-btn" onclick="pawprintSidebar.logout()">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-cards">
|
||||
${users.map(user => `
|
||||
<div class="user-card" data-user-id="${user.id}" onclick="pawprintSidebar.loginAs('${user.id}')">
|
||||
<div class="icon">${user.icon}</div>
|
||||
<div class="info">
|
||||
<span class="label">${user.label}</span>
|
||||
<span class="role">${user.role}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Environment Info Panel -->
|
||||
<div class="panel">
|
||||
<h3>🌍 Environment</h3>
|
||||
<div style="font-size: 12px; opacity: 0.8;">
|
||||
<div style="margin-bottom: 8px;">
|
||||
<strong>Backend:</strong><br>
|
||||
<code style="font-size: 11px;">${this.config.wrapper.environment.backend_url}</code>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Frontend:</strong><br>
|
||||
<code style="font-size: 11px;">${this.config.wrapper.environment.frontend_url}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
Pawprint Dev Tools
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Toggle button
|
||||
this.toggleBtn.addEventListener('click', () => this.toggle());
|
||||
|
||||
// Keyboard shortcut: Ctrl+Shift+P
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey && e.shiftKey && e.key === 'P') {
|
||||
e.preventDefault();
|
||||
this.toggle();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.sidebar.classList.toggle('expanded');
|
||||
this.saveSidebarState();
|
||||
}
|
||||
|
||||
saveSidebarState() {
|
||||
const isExpanded = this.sidebar.classList.contains('expanded');
|
||||
localStorage.setItem('pawprint_sidebar_expanded', isExpanded);
|
||||
}
|
||||
|
||||
loadSidebarState() {
|
||||
const isExpanded = localStorage.getItem('pawprint_sidebar_expanded') === 'true';
|
||||
if (isExpanded) {
|
||||
this.sidebar.classList.add('expanded');
|
||||
}
|
||||
}
|
||||
|
||||
showStatus(message, type = 'info') {
|
||||
const container = document.getElementById('status-container');
|
||||
const statusDiv = document.createElement('div');
|
||||
statusDiv.className = `status-message ${type}`;
|
||||
statusDiv.textContent = message;
|
||||
container.innerHTML = '';
|
||||
container.appendChild(statusDiv);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
statusDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
async loginAs(userId) {
|
||||
const user = this.config.wrapper.users.find(u => u.id === userId);
|
||||
if (!user) return;
|
||||
|
||||
this.showStatus(`Logging in as ${user.label}... ⏳`, 'info');
|
||||
|
||||
try {
|
||||
const backendUrl = this.config.wrapper.environment.backend_url;
|
||||
const response = await fetch(`${backendUrl}/api/token/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: user.username,
|
||||
password: user.password
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Login failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Store tokens
|
||||
localStorage.setItem('access_token', data.access);
|
||||
localStorage.setItem('refresh_token', data.refresh);
|
||||
|
||||
// Store user info
|
||||
localStorage.setItem('user_info', JSON.stringify({
|
||||
username: user.username,
|
||||
label: user.label,
|
||||
role: data.details?.role || user.role
|
||||
}));
|
||||
|
||||
this.showStatus(`✓ Logged in as ${user.label}`, 'success');
|
||||
this.currentUser = user;
|
||||
this.updateCurrentUserDisplay();
|
||||
|
||||
// Reload page after short delay
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Pawprint] Login error:', error);
|
||||
this.showStatus(`✗ Login failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
logout() {
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
localStorage.removeItem('user_info');
|
||||
|
||||
this.showStatus('✓ Logged out', 'success');
|
||||
this.currentUser = null;
|
||||
this.updateCurrentUserDisplay();
|
||||
|
||||
// Reload page after short delay
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
checkCurrentUser() {
|
||||
const userInfo = localStorage.getItem('user_info');
|
||||
if (userInfo) {
|
||||
try {
|
||||
this.currentUser = JSON.parse(userInfo);
|
||||
this.updateCurrentUserDisplay();
|
||||
} catch (error) {
|
||||
console.error('[Pawprint] Failed to parse user info:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateCurrentUserDisplay() {
|
||||
const display = document.getElementById('current-user-display');
|
||||
const username = document.getElementById('current-username');
|
||||
|
||||
if (this.currentUser) {
|
||||
display.style.display = 'block';
|
||||
username.textContent = this.currentUser.username;
|
||||
|
||||
// Highlight active user card
|
||||
document.querySelectorAll('.user-card').forEach(card => {
|
||||
card.classList.remove('active');
|
||||
});
|
||||
|
||||
const activeCard = document.querySelector(`.user-card[data-user-id="${this.getUserIdByUsername(this.currentUser.username)}"]`);
|
||||
if (activeCard) {
|
||||
activeCard.classList.add('active');
|
||||
}
|
||||
} else {
|
||||
display.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
getUserIdByUsername(username) {
|
||||
const user = this.config.wrapper.users.find(u => u.username === username);
|
||||
return user ? user.id : null;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize sidebar when DOM is ready
|
||||
const pawprintSidebar = new PawprintSidebar();
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => pawprintSidebar.init());
|
||||
} else {
|
||||
pawprintSidebar.init();
|
||||
}
|
||||
|
||||
console.log('[Pawprint] Sidebar script loaded');
|
||||
Reference in New Issue
Block a user