test: trigger pipeline

This commit is contained in:
buenosairesam
2026-01-02 19:05:57 -03:00
parent 9e5cbbad1f
commit 56f720ca92
41 changed files with 78 additions and 3252 deletions

311
station/tools/sbwrapper/README.md Executable file
View 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.

View File

@@ -0,0 +1,40 @@
{
"room_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"
}
}
}

View 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;">
&lt;link rel="stylesheet" href="/wrapper/sidebar.css"&gt;<br>
&lt;script src="/wrapper/sidebar.js"&gt;&lt;/script&gt;
</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>

View 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%;
}
}

View 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');