ciyidogan commited on
Commit
3b93905
·
verified ·
1 Parent(s): 2d25d41

Upload 18 files

Browse files
flare-ui/ReadMe.md ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Flare Admin UI Setup
2
+
3
+ ## Quick Start (HuggingFace Deployment)
4
+
5
+ Just push all files to HuggingFace Space. The Dockerfile will handle everything:
6
+ - Build Angular UI
7
+ - Install Python dependencies
8
+ - Serve both UI and API on port 7860
9
+
10
+ ## Local Development
11
+
12
+ ### Backend Setup
13
+ ```bash
14
+ # Install Python dependencies
15
+ pip install -r requirements.txt
16
+
17
+ # Set encryption key (if needed)
18
+ export FLARE_TOKEN_KEY="your-32-byte-base64-key"
19
+
20
+ # Run backend
21
+ python app.py
22
+ ```
23
+
24
+ ### Frontend Setup (for development only)
25
+ ```bash
26
+ # Navigate to UI directory
27
+ cd flare-ui
28
+
29
+ # Install dependencies
30
+ npm install
31
+
32
+ # Run development server (proxies to backend on port 7860)
33
+ npm start
34
+
35
+ # Build for production (creates static/ directory)
36
+ npm run build
37
+ ```
38
+
39
+ ## Docker Deployment
40
+
41
+ ```bash
42
+ # Build and run
43
+ docker build -t flare-admin .
44
+ docker run -p 7860:7860 flare-admin
45
+ ```
46
+
47
+ Access at `http://localhost:7860`
48
+
49
+ ## Default Login
50
+
51
+ - Username: `admin`
52
+ - Password: `admin`
53
+
54
+ ## Creating Admin User
55
+
56
+ To create a new admin user with proper password hash:
57
+
58
+ ```python
59
+ import hashlib
60
+
61
+ password = "your-password"
62
+ password_hash = hashlib.sha256(password.encode()).hexdigest()
63
+ print(f"Password hash: {password_hash}")
64
+ ```
65
+
66
+ Add the user to `service_config.jsonc`:
67
+ ```json
68
+ {
69
+ "config": {
70
+ "users": [
71
+ {
72
+ "username": "newuser",
73
+ "password_hash": "your-hash-here",
74
+ "salt": "random_salt_string"
75
+ }
76
+ ]
77
+ }
78
+ }
79
+ ```
80
+
81
+ ## Project Structure
82
+
83
+ ```
84
+ /
85
+ ├── app.py # Main FastAPI application
86
+ ├── admin_routes.py # Admin API endpoints
87
+ ├── chat_handler.py # Chat functionality
88
+ ├── service_config.jsonc # Configuration file
89
+ ├── Dockerfile # Handles everything for deployment
90
+ ├── flare-ui/ # Angular UI source
91
+ │ ├── src/
92
+ │ │ ├── app/
93
+ │ │ │ ├── components/
94
+ │ │ │ ├── services/
95
+ │ │ │ └── guards/
96
+ │ │ └── index.html
97
+ │ └── package.json
98
+ └── static/ # Built UI files (auto-generated by Docker)
99
+ ```
100
+
101
+ ## Features Implemented
102
+
103
+ - ✅ User authentication with JWT
104
+ - ✅ Environment configuration
105
+ - ✅ Project management
106
+ - ✅ Version control
107
+ - ✅ API management
108
+ - ✅ Activity logging
109
+ - ✅ Race condition handling
110
+ - ✅ Import/Export functionality
111
+
112
+ ## TODO
113
+
114
+ - [ ] User Info tab (password change)
115
+ - [ ] APIs tab (CRUD operations)
116
+ - [ ] Projects tab (full CRUD)
117
+ - [ ] Test tab implementation
118
+ - [ ] Intent/Parameter dialogs
119
+ - [ ] Version comparison
120
+ - [ ] Auto-save for drafts
121
+ - [ ] Keyboard shortcuts
122
+
123
+ ---
124
+
125
+ ## ⚠️ Production Deployment Note
126
+
127
+ **This setup is optimized for HuggingFace Spaces and development environments.** For production on-premise deployment, a more robust architecture will be implemented.
128
+
129
+ ### Planned Production Architecture:
130
+
131
+ #### 1. **Web Server Layer**
132
+ - **Nginx** as reverse proxy and static file server
133
+ - SSL/TLS termination
134
+ - Request rate limiting and security headers
135
+ - Gzip compression for static assets
136
+
137
+ #### 2. **Application Layer**
138
+ - **Gunicorn/Uvicorn** workers for Python ASGI
139
+ - Process management with **Supervisor** or **systemd**
140
+ - Horizontal scaling with multiple worker processes
141
+ - Health check endpoints for monitoring
142
+
143
+ #### 3. **Session & Cache Layer**
144
+ - **Redis** for distributed session storage
145
+ - Centralized cache for LLM responses
146
+ - Session affinity for WebSocket connections
147
+
148
+ #### 4. **Database Layer** (Optional)
149
+ - **PostgreSQL** for configuration storage (replacing JSON file)
150
+ - Backup and replication strategies
151
+ - Migration tools for schema updates
152
+
153
+ #### 5. **Monitoring & Logging**
154
+ - **Prometheus** + **Grafana** for metrics
155
+ - **ELK Stack** (Elasticsearch, Logstash, Kibana) for log aggregation
156
+ - Application Performance Monitoring (APM)
157
+ - Alert configuration for critical events
158
+
159
+ #### 6. **Deployment Options**
160
+
161
+ **Option A: Docker Compose** (Small-Medium Scale)
162
+ ```yaml
163
+ services:
164
+ nginx:
165
+ image: nginx:alpine
166
+ volumes:
167
+ - ./nginx.conf:/etc/nginx/nginx.conf
168
+ - ./static:/usr/share/nginx/html
169
+ ports:
170
+ - "80:80"
171
+ - "443:443"
172
+
173
+ app:
174
+ image: flare-admin:production
175
+ scale: 3 # 3 instances
176
+ environment:
177
+ - REDIS_URL=redis://redis:6379
178
+
179
+ redis:
180
+ image: redis:alpine
181
+ volumes:
182
+ - redis-data:/data
183
+ ```
184
+
185
+ **Option B: Kubernetes** (Large Scale)
186
+ - Helm charts for easy deployment
187
+ - Horizontal Pod Autoscaler (HPA)
188
+ - Ingress controller for routing
189
+ - Persistent Volume Claims for data
190
+ - Secrets management for credentials
191
+
192
+ #### 7. **Security Considerations**
193
+ - JWT token rotation and refresh
194
+ - API rate limiting per user
195
+ - Input validation and sanitization
196
+ - Regular security audits
197
+ - Compliance with data protection regulations
198
+
199
+ #### 8. **Backup & Disaster Recovery**
200
+ - Automated daily backups
201
+ - Point-in-time recovery
202
+ - Geo-redundant storage
203
+ - Disaster recovery procedures
204
+ - RTO/RPO targets defined
205
+
206
+ ### Production Deployment Timeline:
207
+ 1. **Phase 1**: Current setup (HF Spaces, development)
208
+ 2. **Phase 2**: Docker Compose setup for on-premise pilot
209
+ 3. **Phase 3**: Full production architecture with monitoring
210
+ 4. **Phase 4**: Kubernetes deployment for enterprise scale
211
+
212
+ A comprehensive `DEPLOYMENT.md` guide will be created when transitioning to production architecture.
flare-ui/angular.json ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3
+ "version": 1,
4
+ "newProjectRoot": "projects",
5
+ "projects": {
6
+ "flare-ui": {
7
+ "projectType": "application",
8
+ "schematics": {
9
+ "@schematics/angular:component": {
10
+ "style": "scss"
11
+ }
12
+ },
13
+ "root": "",
14
+ "sourceRoot": "src",
15
+ "prefix": "app",
16
+ "architect": {
17
+ "build": {
18
+ "builder": "@angular-devkit/build-angular:browser",
19
+ "options": {
20
+ "outputPath": "dist/flare-ui",
21
+ "index": "src/index.html",
22
+ "main": "src/main.ts",
23
+ "polyfills": [
24
+ "zone.js"
25
+ ],
26
+ "tsConfig": "tsconfig.app.json",
27
+ "inlineStyleLanguage": "scss",
28
+ "assets": [
29
+ "src/favicon.ico",
30
+ "src/assets"
31
+ ],
32
+ "styles": [
33
+ "src/styles.scss"
34
+ ],
35
+ "scripts": []
36
+ },
37
+ "configurations": {
38
+ "production": {
39
+ "budgets": [
40
+ {
41
+ "type": "initial",
42
+ "maximumWarning": "500kb",
43
+ "maximumError": "1mb"
44
+ },
45
+ {
46
+ "type": "anyComponentStyle",
47
+ "maximumWarning": "2kb",
48
+ "maximumError": "4kb"
49
+ }
50
+ ],
51
+ "outputHashing": "all"
52
+ },
53
+ "development": {
54
+ "buildOptimizer": false,
55
+ "optimization": false,
56
+ "vendorChunk": true,
57
+ "extractLicenses": false,
58
+ "sourceMap": true,
59
+ "namedChunks": true
60
+ }
61
+ },
62
+ "defaultConfiguration": "production"
63
+ },
64
+ "serve": {
65
+ "builder": "@angular-devkit/build-angular:dev-server",
66
+ "configurations": {
67
+ "production": {
68
+ "buildTarget": "flare-ui:build:production"
69
+ },
70
+ "development": {
71
+ "buildTarget": "flare-ui:build:development"
72
+ }
73
+ },
74
+ "defaultConfiguration": "development"
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
flare-ui/package.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "flare-ui",
3
+ "version": "0.1.0",
4
+ "scripts": {
5
+ "ng": "ng",
6
+ "start": "ng serve",
7
+ "build": "ng build --configuration production --output-path ../static",
8
+ "watch": "ng build --watch --configuration development",
9
+ "test": "ng test"
10
+ },
11
+ "private": true,
12
+ "dependencies": {
13
+ "@angular/animations": "^17.0.0",
14
+ "@angular/common": "^17.0.0",
15
+ "@angular/compiler": "^17.0.0",
16
+ "@angular/core": "^17.0.0",
17
+ "@angular/forms": "^17.0.0",
18
+ "@angular/platform-browser": "^17.0.0",
19
+ "@angular/platform-browser-dynamic": "^17.0.0",
20
+ "@angular/router": "^17.0.0",
21
+ "rxjs": "~7.8.0",
22
+ "tslib": "^2.3.0",
23
+ "zone.js": "~0.14.2"
24
+ },
25
+ "devDependencies": {
26
+ "@angular-devkit/build-angular": "^17.0.0",
27
+ "@angular/cli": "^17.0.0",
28
+ "@angular/compiler-cli": "^17.0.0",
29
+ "@types/node": "^20.0.0",
30
+ "typescript": "~5.2.0"
31
+ }
32
+ }
flare-ui/src/app/app.component.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { RouterOutlet } from '@angular/router';
4
+
5
+ @Component({
6
+ selector: 'app-root',
7
+ standalone: true,
8
+ imports: [CommonModule, RouterOutlet],
9
+ template: `
10
+ <router-outlet></router-outlet>
11
+ `,
12
+ styles: [`
13
+ :host {
14
+ display: block;
15
+ height: 100vh;
16
+ }
17
+ `]
18
+ })
19
+ export class AppComponent {
20
+ title = 'Flare Administration';
21
+ }
flare-ui/src/app/app.routes.ts ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Routes } from '@angular/router';
2
+ import { authGuard } from './guards/auth.guard';
3
+
4
+ export const routes: Routes = [
5
+ {
6
+ path: 'login',
7
+ loadComponent: () => import('./components/login/login.component').then(m => m.LoginComponent)
8
+ },
9
+ {
10
+ path: '',
11
+ loadComponent: () => import('./components/main/main.component').then(m => m.MainComponent),
12
+ canActivate: [authGuard],
13
+ children: [
14
+ {
15
+ path: 'user-info',
16
+ loadComponent: () => import('./components/user-info/user-info.component').then(m => m.UserInfoComponent)
17
+ },
18
+ {
19
+ path: 'environment',
20
+ loadComponent: () => import('./components/environment/environment.component').then(m => m.EnvironmentComponent)
21
+ },
22
+ {
23
+ path: 'apis',
24
+ loadComponent: () => import('./components/apis/apis.component').then(m => m.ApisComponent)
25
+ },
26
+ {
27
+ path: 'projects',
28
+ loadComponent: () => import('./components/projects/projects.component').then(m => m.ProjectsComponent)
29
+ },
30
+ {
31
+ path: 'test',
32
+ loadComponent: () => import('./components/test/test.component').then(m => m.TestComponent)
33
+ },
34
+ {
35
+ path: '',
36
+ redirectTo: 'projects',
37
+ pathMatch: 'full'
38
+ }
39
+ ]
40
+ },
41
+ {
42
+ path: '**',
43
+ redirectTo: ''
44
+ }
45
+ ];
flare-ui/src/app/components/activity-log/activity-log.component.ts ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, EventEmitter, Output, inject, OnInit } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { HttpClient } from '@angular/common/http';
4
+
5
+ interface ActivityLog {
6
+ id: number;
7
+ timestamp: string;
8
+ user: string;
9
+ action: string;
10
+ entity_type: string;
11
+ entity_id: any;
12
+ entity_name: string;
13
+ details?: string;
14
+ }
15
+
16
+ @Component({
17
+ selector: 'app-activity-log',
18
+ standalone: true,
19
+ imports: [CommonModule],
20
+ template: `
21
+ <div class="activity-log-dropdown" (click)="$event.stopPropagation()">
22
+ <div class="activity-header">
23
+ <h3>🔔 Recent Activities</h3>
24
+ <button class="close-btn" (click)="close.emit()">×</button>
25
+ </div>
26
+ <div class="activity-list">
27
+ @if (loading) {
28
+ <div class="loading">Loading...</div>
29
+ } @else if (activities.length === 0) {
30
+ <div class="empty">No recent activities</div>
31
+ } @else {
32
+ @for (activity of activities; track activity.id) {
33
+ <div class="activity-item">
34
+ <div class="activity-time">{{ getRelativeTime(activity.timestamp) }}</div>
35
+ <div class="activity-content">
36
+ <strong>{{ activity.user }}</strong> {{ getActionText(activity) }}
37
+ <em>{{ activity.entity_name }}</em>
38
+ </div>
39
+ </div>
40
+ }
41
+ }
42
+ </div>
43
+ <div class="activity-footer">
44
+ <button class="btn btn-secondary" (click)="loadMore()">View All Activities</button>
45
+ </div>
46
+ </div>
47
+ `,
48
+ styles: [`
49
+ .activity-log-dropdown {
50
+ position: absolute;
51
+ top: 100%;
52
+ right: 0;
53
+ width: 350px;
54
+ background: white;
55
+ border: 1px solid #dee2e6;
56
+ border-radius: 8px;
57
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
58
+ z-index: 1000;
59
+ margin-top: 0.5rem;
60
+ }
61
+
62
+ .activity-header {
63
+ padding: 1rem;
64
+ border-bottom: 1px solid #dee2e6;
65
+ display: flex;
66
+ justify-content: space-between;
67
+ align-items: center;
68
+
69
+ h3 {
70
+ margin: 0;
71
+ font-size: 1.1rem;
72
+ }
73
+
74
+ .close-btn {
75
+ background: none;
76
+ border: none;
77
+ font-size: 1.5rem;
78
+ cursor: pointer;
79
+ color: #6c757d;
80
+
81
+ &:hover {
82
+ color: #333;
83
+ }
84
+ }
85
+ }
86
+
87
+ .activity-list {
88
+ max-height: 300px;
89
+ overflow-y: auto;
90
+ }
91
+
92
+ .activity-item {
93
+ padding: 0.75rem 1rem;
94
+ border-bottom: 1px solid #f0f0f0;
95
+
96
+ &:hover {
97
+ background-color: #f8f9fa;
98
+ }
99
+
100
+ .activity-time {
101
+ font-size: 0.85rem;
102
+ color: #6c757d;
103
+ margin-bottom: 0.25rem;
104
+ }
105
+
106
+ .activity-content {
107
+ font-size: 0.9rem;
108
+
109
+ em {
110
+ color: #007bff;
111
+ font-style: normal;
112
+ }
113
+ }
114
+ }
115
+
116
+ .activity-footer {
117
+ padding: 0.75rem;
118
+ border-top: 1px solid #dee2e6;
119
+ text-align: center;
120
+ }
121
+
122
+ .loading, .empty {
123
+ padding: 2rem;
124
+ text-align: center;
125
+ color: #6c757d;
126
+ }
127
+ `]
128
+ })
129
+ export class ActivityLogComponent implements OnInit {
130
+ @Output() close = new EventEmitter<void>();
131
+
132
+ private http = inject(HttpClient);
133
+
134
+ activities: ActivityLog[] = [];
135
+ loading = true;
136
+
137
+ ngOnInit() {
138
+ this.loadActivities();
139
+ }
140
+
141
+ loadActivities() {
142
+ this.loading = true;
143
+ this.http.get<ActivityLog[]>('/api/activity-log?limit=10')
144
+ .subscribe({
145
+ next: (data) => {
146
+ this.activities = data;
147
+ this.loading = false;
148
+ },
149
+ error: () => {
150
+ this.loading = false;
151
+ }
152
+ });
153
+ }
154
+
155
+ loadMore() {
156
+ // TODO: Implement full activity log view
157
+ console.log('Load more activities');
158
+ }
159
+
160
+ getRelativeTime(timestamp: string): string {
161
+ const date = new Date(timestamp);
162
+ const now = new Date();
163
+ const diff = now.getTime() - date.getTime();
164
+
165
+ const minutes = Math.floor(diff / 60000);
166
+ const hours = Math.floor(diff / 3600000);
167
+ const days = Math.floor(diff / 86400000);
168
+
169
+ if (minutes < 1) return 'just now';
170
+ if (minutes < 60) return `${minutes} min ago`;
171
+ if (hours < 24) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
172
+ return `${days} day${days > 1 ? 's' : ''} ago`;
173
+ }
174
+
175
+ getActionText(activity: ActivityLog): string {
176
+ const actions: Record<string, string> = {
177
+ 'CREATE_PROJECT': 'created project',
178
+ 'UPDATE_PROJECT': 'updated project',
179
+ 'DELETE_PROJECT': 'deleted project',
180
+ 'PUBLISH_VERSION': 'published version',
181
+ 'CREATE_VERSION': 'created version',
182
+ 'UPDATE_VERSION': 'updated version',
183
+ 'CREATE_API': 'created API',
184
+ 'UPDATE_API': 'updated API',
185
+ 'DELETE_API': 'deleted API',
186
+ 'UPDATE_ENVIRONMENT': 'updated environment',
187
+ 'IMPORT_PROJECT': 'imported project'
188
+ };
189
+
190
+ return actions[activity.action] || activity.action.toLowerCase().replace('_', ' ');
191
+ }
192
+ }
flare-ui/src/app/components/environment/environment.component.ts ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, inject, OnInit } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { FormsModule } from '@angular/forms';
4
+ import { ApiService, Environment } from '../../services/api.service';
5
+
6
+ @Component({
7
+ selector: 'app-environment',
8
+ standalone: true,
9
+ imports: [CommonModule, FormsModule],
10
+ template: `
11
+ <div class="environment-container">
12
+ <h2>Environment Configuration</h2>
13
+
14
+ <div class="card">
15
+ <div class="card-body">
16
+ <form (ngSubmit)="save()" #envForm="ngForm">
17
+ <div class="form-group">
18
+ <label for="workMode">Work Mode *</label>
19
+ <select
20
+ id="workMode"
21
+ name="workMode"
22
+ [(ngModel)]="environment.work_mode"
23
+ (change)="onWorkModeChange()"
24
+ required
25
+ [disabled]="loading"
26
+ >
27
+ <option value="hfcloud">HF Cloud</option>
28
+ <option value="cloud">Cloud</option>
29
+ <option value="on-premise">On-Premise</option>
30
+ </select>
31
+ </div>
32
+
33
+ <div class="form-group">
34
+ <label for="cloudToken">Cloud Token</label>
35
+ <input
36
+ type="password"
37
+ id="cloudToken"
38
+ name="cloudToken"
39
+ [(ngModel)]="environment.cloud_token"
40
+ [disabled]="loading || environment.work_mode === 'on-premise'"
41
+ placeholder="Enter cloud token"
42
+ >
43
+ <small class="text-muted">Required for HF Cloud and Cloud modes</small>
44
+ </div>
45
+
46
+ <div class="form-group">
47
+ <label for="sparkEndpoint">Spark Endpoint *</label>
48
+ <div class="input-with-button">
49
+ <input
50
+ type="url"
51
+ id="sparkEndpoint"
52
+ name="sparkEndpoint"
53
+ [(ngModel)]="environment.spark_endpoint"
54
+ required
55
+ [disabled]="loading"
56
+ placeholder="https://spark-service.example.com"
57
+ >
58
+ <button
59
+ type="button"
60
+ class="btn btn-secondary"
61
+ (click)="testConnection()"
62
+ [disabled]="loading || !environment.spark_endpoint"
63
+ >
64
+ Test Connection
65
+ </button>
66
+ </div>
67
+ </div>
68
+
69
+ @if (message) {
70
+ <div class="alert" [class.alert-success]="!isError" [class.alert-danger]="isError">
71
+ {{ message }}
72
+ </div>
73
+ }
74
+
75
+ <div class="form-actions">
76
+ <button
77
+ type="submit"
78
+ class="btn btn-primary"
79
+ [disabled]="loading || !envForm.valid"
80
+ >
81
+ @if (saving) {
82
+ <span class="spinner"></span> Saving...
83
+ } @else {
84
+ Save
85
+ }
86
+ </button>
87
+ <button
88
+ type="button"
89
+ class="btn btn-secondary"
90
+ (click)="reloadFromSpark()"
91
+ [disabled]="loading"
92
+ >
93
+ Reload from Spark
94
+ </button>
95
+ </div>
96
+ </form>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ `,
101
+ styles: [`
102
+ .environment-container {
103
+ h2 {
104
+ margin-bottom: 1.5rem;
105
+ }
106
+ }
107
+
108
+ .input-with-button {
109
+ display: flex;
110
+ gap: 0.5rem;
111
+
112
+ input {
113
+ flex: 1;
114
+ }
115
+ }
116
+
117
+ .form-actions {
118
+ display: flex;
119
+ gap: 0.5rem;
120
+ margin-top: 1.5rem;
121
+ }
122
+
123
+ .text-muted {
124
+ color: #6c757d;
125
+ font-size: 0.875rem;
126
+ }
127
+ `]
128
+ })
129
+ export class EnvironmentComponent implements OnInit {
130
+ private apiService = inject(ApiService);
131
+
132
+ environment: Environment = {
133
+ work_mode: 'hfcloud',
134
+ cloud_token: '',
135
+ spark_endpoint: ''
136
+ };
137
+
138
+ loading = true;
139
+ saving = false;
140
+ message = '';
141
+ isError = false;
142
+
143
+ ngOnInit() {
144
+ this.loadEnvironment();
145
+ }
146
+
147
+ loadEnvironment() {
148
+ this.loading = true;
149
+ this.apiService.getEnvironment().subscribe({
150
+ next: (env) => {
151
+ this.environment = env;
152
+ this.loading = false;
153
+ },
154
+ error: (err) => {
155
+ this.showMessage('Failed to load environment configuration', true);
156
+ this.loading = false;
157
+ }
158
+ });
159
+ }
160
+
161
+ onWorkModeChange() {
162
+ if (this.environment.work_mode === 'on-premise') {
163
+ this.environment.cloud_token = '';
164
+ }
165
+ }
166
+
167
+ save() {
168
+ this.saving = true;
169
+ this.message = '';
170
+
171
+ this.apiService.updateEnvironment(this.environment).subscribe({
172
+ next: () => {
173
+ this.showMessage('Environment configuration saved successfully', false);
174
+ this.saving = false;
175
+ },
176
+ error: (err) => {
177
+ this.showMessage(err.error?.detail || 'Failed to save configuration', true);
178
+ this.saving = false;
179
+ }
180
+ });
181
+ }
182
+
183
+ testConnection() {
184
+ // TODO: Implement connection test
185
+ this.showMessage('Testing connection to Spark endpoint...', false);
186
+ setTimeout(() => {
187
+ this.showMessage('Connection successful!', false);
188
+ }, 1000);
189
+ }
190
+
191
+ reloadFromSpark() {
192
+ // TODO: Implement reload from Spark
193
+ this.showMessage('Reloading configuration from Spark...', false);
194
+ setTimeout(() => {
195
+ this.loadEnvironment();
196
+ this.showMessage('Configuration reloaded', false);
197
+ }, 1000);
198
+ }
199
+
200
+ private showMessage(message: string, isError: boolean) {
201
+ this.message = message;
202
+ this.isError = isError;
203
+
204
+ if (!isError) {
205
+ setTimeout(() => {
206
+ this.message = '';
207
+ }, 5000);
208
+ }
209
+ }
210
+ }
flare-ui/src/app/components/login/login.component.ts ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, inject } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { FormsModule } from '@angular/forms';
4
+ import { Router } from '@angular/router';
5
+ import { AuthService } from '../../services/auth.service';
6
+
7
+ @Component({
8
+ selector: 'app-login',
9
+ standalone: true,
10
+ imports: [CommonModule, FormsModule],
11
+ template: `
12
+ <div class="login-container">
13
+ <div class="login-card">
14
+ <h1>Flare Administration</h1>
15
+ <form (ngSubmit)="login()" #loginForm="ngForm">
16
+ <div class="form-group">
17
+ <label for="username">Username</label>
18
+ <input
19
+ type="text"
20
+ id="username"
21
+ name="username"
22
+ [(ngModel)]="username"
23
+ required
24
+ [disabled]="loading"
25
+ >
26
+ </div>
27
+ <div class="form-group">
28
+ <label for="password">Password</label>
29
+ <input
30
+ type="password"
31
+ id="password"
32
+ name="password"
33
+ [(ngModel)]="password"
34
+ required
35
+ [disabled]="loading"
36
+ >
37
+ </div>
38
+ @if (error) {
39
+ <div class="alert alert-danger">{{ error }}</div>
40
+ }
41
+ <button
42
+ type="submit"
43
+ class="btn btn-primary w-100"
44
+ [disabled]="loading || !loginForm.valid"
45
+ >
46
+ @if (loading) {
47
+ <span class="spinner"></span> Logging in...
48
+ } @else {
49
+ Login
50
+ }
51
+ </button>
52
+ </form>
53
+ </div>
54
+ </div>
55
+ `,
56
+ styles: [`
57
+ .login-container {
58
+ min-height: 100vh;
59
+ display: flex;
60
+ align-items: center;
61
+ justify-content: center;
62
+ background-color: #f5f5f5;
63
+ }
64
+
65
+ .login-card {
66
+ background: white;
67
+ padding: 2rem;
68
+ border-radius: 8px;
69
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
70
+ width: 100%;
71
+ max-width: 400px;
72
+
73
+ h1 {
74
+ text-align: center;
75
+ margin-bottom: 2rem;
76
+ color: #333;
77
+ font-size: 1.5rem;
78
+ }
79
+ }
80
+
81
+ .w-100 {
82
+ width: 100%;
83
+ }
84
+
85
+ button {
86
+ display: flex;
87
+ align-items: center;
88
+ justify-content: center;
89
+ gap: 0.5rem;
90
+ }
91
+ `]
92
+ })
93
+ export class LoginComponent {
94
+ private authService = inject(AuthService);
95
+ private router = inject(Router);
96
+
97
+ username = '';
98
+ password = '';
99
+ loading = false;
100
+ error = '';
101
+
102
+ async login() {
103
+ this.loading = true;
104
+ this.error = '';
105
+
106
+ try {
107
+ await this.authService.login(this.username, this.password).toPromise();
108
+ this.router.navigate(['/']);
109
+ } catch (err: any) {
110
+ this.error = err.error?.detail || 'Invalid credentials';
111
+ } finally {
112
+ this.loading = false;
113
+ }
114
+ }
115
+ }
flare-ui/src/app/components/main/main.component.ts ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, inject } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
4
+ import { AuthService } from '../../services/auth.service';
5
+ import { ActivityLogComponent } from '../activity-log/activity-log.component';
6
+
7
+ @Component({
8
+ selector: 'app-main',
9
+ standalone: true,
10
+ imports: [CommonModule, RouterLink, RouterLinkActive, RouterOutlet, ActivityLogComponent],
11
+ template: `
12
+ <div class="main-layout">
13
+ <header class="header">
14
+ <div class="header-content">
15
+ <h1>Flare Administration</h1>
16
+ <div class="header-actions">
17
+ <span class="username">{{ username }}</span>
18
+ <button class="notification-btn" (click)="toggleActivityLog()">
19
+ 🔔
20
+ @if (showActivityLog) {
21
+ <app-activity-log (close)="toggleActivityLog()"></app-activity-log>
22
+ }
23
+ </button>
24
+ <button class="btn btn-secondary" (click)="logout()">Logout</button>
25
+ </div>
26
+ </div>
27
+ </header>
28
+
29
+ <nav class="tabs">
30
+ <a
31
+ routerLink="/user-info"
32
+ routerLinkActive="active"
33
+ class="tab"
34
+ >
35
+ User Info
36
+ </a>
37
+ <a
38
+ routerLink="/environment"
39
+ routerLinkActive="active"
40
+ class="tab"
41
+ >
42
+ Environment
43
+ </a>
44
+ <a
45
+ routerLink="/apis"
46
+ routerLinkActive="active"
47
+ class="tab"
48
+ >
49
+ APIs
50
+ </a>
51
+ <a
52
+ routerLink="/projects"
53
+ routerLinkActive="active"
54
+ class="tab"
55
+ >
56
+ Projects
57
+ </a>
58
+ <a
59
+ routerLink="/test"
60
+ routerLinkActive="active"
61
+ class="tab"
62
+ >
63
+ Test
64
+ </a>
65
+ </nav>
66
+
67
+ <main class="content">
68
+ <router-outlet></router-outlet>
69
+ </main>
70
+ </div>
71
+ `,
72
+ styles: [`
73
+ .main-layout {
74
+ height: 100vh;
75
+ display: flex;
76
+ flex-direction: column;
77
+ }
78
+
79
+ .header {
80
+ background-color: #fff;
81
+ border-bottom: 1px solid #dee2e6;
82
+ padding: 1rem 0;
83
+
84
+ .header-content {
85
+ max-width: 1200px;
86
+ margin: 0 auto;
87
+ padding: 0 20px;
88
+ display: flex;
89
+ justify-content: space-between;
90
+ align-items: center;
91
+
92
+ h1 {
93
+ font-size: 1.5rem;
94
+ color: #333;
95
+ margin: 0;
96
+ }
97
+ }
98
+
99
+ .header-actions {
100
+ display: flex;
101
+ align-items: center;
102
+ gap: 1rem;
103
+
104
+ .username {
105
+ font-weight: 500;
106
+ color: #495057;
107
+ }
108
+
109
+ .notification-btn {
110
+ position: relative;
111
+ background: none;
112
+ border: none;
113
+ font-size: 1.25rem;
114
+ cursor: pointer;
115
+ padding: 0.25rem;
116
+
117
+ &:hover {
118
+ opacity: 0.7;
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ .tabs {
125
+ background-color: #fff;
126
+ border-bottom: 2px solid #dee2e6;
127
+ max-width: 1200px;
128
+ width: 100%;
129
+ margin: 0 auto;
130
+ padding: 0 20px;
131
+ display: flex;
132
+
133
+ a {
134
+ text-decoration: none;
135
+ }
136
+ }
137
+
138
+ .content {
139
+ flex: 1;
140
+ overflow-y: auto;
141
+ padding: 2rem 0;
142
+
143
+ > * {
144
+ max-width: 1200px;
145
+ margin: 0 auto;
146
+ padding: 0 20px;
147
+ }
148
+ }
149
+ `]
150
+ })
151
+ export class MainComponent {
152
+ private authService = inject(AuthService);
153
+
154
+ username = this.authService.getUsername() || '';
155
+ showActivityLog = false;
156
+
157
+ logout() {
158
+ this.authService.logout();
159
+ }
160
+
161
+ toggleActivityLog() {
162
+ this.showActivityLog = !this.showActivityLog;
163
+ }
164
+ }
flare-ui/src/app/guards/auth.guard.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { inject } from '@angular/core';
2
+ import { Router } from '@angular/router';
3
+ import { AuthService } from '../services/auth.service';
4
+
5
+ export const authGuard = () => {
6
+ const authService = inject(AuthService);
7
+ const router = inject(Router);
8
+
9
+ if (authService.isLoggedIn()) {
10
+ return true;
11
+ }
12
+
13
+ return router.createUrlTree(['/login']);
14
+ };
flare-ui/src/app/interceptors/auth.interceptor.ts ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
2
+ import { inject } from '@angular/core';
3
+ import { Router } from '@angular/router';
4
+ import { catchError, throwError } from 'rxjs';
5
+ import { AuthService } from '../services/auth.service';
6
+
7
+ export const authInterceptor: HttpInterceptorFn = (req, next) => {
8
+ const authService = inject(AuthService);
9
+ const router = inject(Router);
10
+
11
+ // Skip auth for login endpoint
12
+ if (req.url.includes('/api/login')) {
13
+ return next(req);
14
+ }
15
+
16
+ // Add auth token to requests
17
+ const token = authService.getToken();
18
+ if (token) {
19
+ req = req.clone({
20
+ setHeaders: {
21
+ Authorization: `Bearer ${token}`
22
+ }
23
+ });
24
+ }
25
+
26
+ return next(req).pipe(
27
+ catchError((error: HttpErrorResponse) => {
28
+ if (error.status === 401) {
29
+ authService.logout();
30
+ router.navigate(['/login']);
31
+ }
32
+ return throwError(() => error);
33
+ })
34
+ );
35
+ };
flare-ui/src/app/services/api.service.ts ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable, inject } from '@angular/core';
2
+ import { HttpClient } from '@angular/common/http';
3
+ import { Observable } from 'rxjs';
4
+
5
+ // Models
6
+ export interface Environment {
7
+ work_mode: string;
8
+ cloud_token: string;
9
+ spark_endpoint: string;
10
+ }
11
+
12
+ export interface Project {
13
+ id: number;
14
+ name: string;
15
+ caption: string;
16
+ enabled: boolean;
17
+ last_version_number: number;
18
+ version_id_counter: number;
19
+ versions: Version[];
20
+ deleted: boolean;
21
+ created_date: string;
22
+ created_by: string;
23
+ last_update_date: string;
24
+ last_update_user: string;
25
+ }
26
+
27
+ export interface Version {
28
+ id: number;
29
+ caption: string;
30
+ published: boolean;
31
+ general_prompt: string;
32
+ llm: LLMConfig;
33
+ intents: Intent[];
34
+ created_date: string;
35
+ created_by: string;
36
+ last_update_date: string;
37
+ last_update_user: string;
38
+ publish_date?: string;
39
+ published_by?: string;
40
+ deleted?: boolean;
41
+ }
42
+
43
+ export interface LLMConfig {
44
+ repo_id: string;
45
+ generation_config: {
46
+ max_new_tokens: number;
47
+ temperature: number;
48
+ top_p: number;
49
+ repetition_penalty: number;
50
+ };
51
+ use_fine_tune: boolean;
52
+ fine_tune_zip: string;
53
+ }
54
+
55
+ export interface Intent {
56
+ name: string;
57
+ caption: string;
58
+ locale: string;
59
+ detection_prompt: string;
60
+ examples: string[];
61
+ parameters: Parameter[];
62
+ action: string;
63
+ fallback_timeout_prompt?: string;
64
+ fallback_error_prompt?: string;
65
+ }
66
+
67
+ export interface Parameter {
68
+ name: string;
69
+ caption: string;
70
+ type: string;
71
+ required: boolean;
72
+ variable_name: string;
73
+ extraction_prompt: string;
74
+ validation_regex?: string;
75
+ invalid_prompt?: string;
76
+ type_error_prompt?: string;
77
+ }
78
+
79
+ export interface API {
80
+ name: string;
81
+ url: string;
82
+ method: string;
83
+ headers: Record<string, string>;
84
+ body_template: any;
85
+ timeout_seconds: number;
86
+ retry: {
87
+ retry_count: number;
88
+ backoff_seconds: number;
89
+ strategy: string;
90
+ };
91
+ proxy?: string;
92
+ auth?: APIAuth;
93
+ response_prompt?: string;
94
+ deleted: boolean;
95
+ created_date: string;
96
+ created_by: string;
97
+ last_update_date: string;
98
+ last_update_user: string;
99
+ }
100
+
101
+ export interface APIAuth {
102
+ enabled: boolean;
103
+ token_endpoint?: string;
104
+ response_token_path?: string;
105
+ token_request_body?: any;
106
+ token_refresh_endpoint?: string;
107
+ token_refresh_body?: any;
108
+ }
109
+
110
+ @Injectable({
111
+ providedIn: 'root'
112
+ })
113
+ export class ApiService {
114
+ private http = inject(HttpClient);
115
+ private baseUrl = '/api';
116
+
117
+ // Environment
118
+ getEnvironment(): Observable<Environment> {
119
+ return this.http.get<Environment>(`${this.baseUrl}/environment`);
120
+ }
121
+
122
+ updateEnvironment(env: Environment): Observable<any> {
123
+ return this.http.put(`${this.baseUrl}/environment`, env);
124
+ }
125
+
126
+ // Projects
127
+ getProjects(includeDeleted = false): Observable<Project[]> {
128
+ return this.http.get<Project[]>(`${this.baseUrl}/projects?include_deleted=${includeDeleted}`);
129
+ }
130
+
131
+ createProject(project: { name: string; caption: string }): Observable<Project> {
132
+ return this.http.post<Project>(`${this.baseUrl}/projects`, project);
133
+ }
134
+
135
+ updateProject(id: number, update: { caption: string; last_update_date: string }): Observable<Project> {
136
+ return this.http.put<Project>(`${this.baseUrl}/projects/${id}`, update);
137
+ }
138
+
139
+ deleteProject(id: number): Observable<any> {
140
+ return this.http.delete(`${this.baseUrl}/projects/${id}`);
141
+ }
142
+
143
+ toggleProject(id: number): Observable<{ enabled: boolean }> {
144
+ return this.http.patch<{ enabled: boolean }>(`${this.baseUrl}/projects/${id}/toggle`, {});
145
+ }
146
+
147
+ exportProject(id: number): Observable<any> {
148
+ return this.http.get(`${this.baseUrl}/projects/${id}/export`);
149
+ }
150
+
151
+ importProject(data: any): Observable<any> {
152
+ return this.http.post(`${this.baseUrl}/projects/import`, data);
153
+ }
154
+
155
+ // Versions
156
+ createVersion(projectId: number, sourceVersionId: number, caption: string): Observable<Version> {
157
+ return this.http.post<Version>(`${this.baseUrl}/projects/${projectId}/versions`, {
158
+ source_version_id: sourceVersionId,
159
+ caption
160
+ });
161
+ }
162
+
163
+ updateVersion(projectId: number, versionId: number, update: any): Observable<Version> {
164
+ return this.http.put<Version>(`${this.baseUrl}/projects/${projectId}/versions/${versionId}`, update);
165
+ }
166
+
167
+ publishVersion(projectId: number, versionId: number): Observable<any> {
168
+ return this.http.post(`${this.baseUrl}/projects/${projectId}/versions/${versionId}/publish`, {});
169
+ }
170
+
171
+ deleteVersion(projectId: number, versionId: number): Observable<any> {
172
+ return this.http.delete(`${this.baseUrl}/projects/${projectId}/versions/${versionId}`);
173
+ }
174
+
175
+ // APIs
176
+ getAPIs(includeDeleted = false): Observable<API[]> {
177
+ return this.http.get<API[]>(`${this.baseUrl}/apis?include_deleted=${includeDeleted}`);
178
+ }
179
+
180
+ createAPI(api: Partial<API>): Observable<API> {
181
+ return this.http.post<API>(`${this.baseUrl}/apis`, api);
182
+ }
183
+
184
+ updateAPI(name: string, update: any): Observable<API> {
185
+ return this.http.put<API>(`${this.baseUrl}/apis/${name}`, update);
186
+ }
187
+
188
+ deleteAPI(name: string): Observable<any> {
189
+ return this.http.delete(`${this.baseUrl}/apis/${name}`);
190
+ }
191
+
192
+ testAPI(api: Partial<API>): Observable<any> {
193
+ return this.http.post(`${this.baseUrl}/apis/test`, api);
194
+ }
195
+
196
+ // Testing
197
+ runTests(testType: string): Observable<any> {
198
+ return this.http.post(`${this.baseUrl}/test/run-all`, { test_type: testType });
199
+ }
200
+
201
+ // Validation
202
+ validateRegex(pattern: string, testValue: string): Observable<{ valid: boolean; matches?: boolean; error?: string }> {
203
+ return this.http.post<any>(`${this.baseUrl}/validate/regex`, { pattern, test_value: testValue });
204
+ }
205
+ }
flare-ui/src/app/services/auth.service.ts ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable, inject } from '@angular/core';
2
+ import { HttpClient } from '@angular/common/http';
3
+ import { Router } from '@angular/router';
4
+ import { BehaviorSubject, Observable, tap } from 'rxjs';
5
+
6
+ interface LoginResponse {
7
+ token: string;
8
+ username: string;
9
+ }
10
+
11
+ @Injectable({
12
+ providedIn: 'root'
13
+ })
14
+ export class AuthService {
15
+ private http = inject(HttpClient);
16
+ private router = inject(Router);
17
+
18
+ private tokenKey = 'flare_token';
19
+ private usernameKey = 'flare_username';
20
+
21
+ private loggedInSubject = new BehaviorSubject<boolean>(this.hasToken());
22
+ public loggedIn$ = this.loggedInSubject.asObservable();
23
+
24
+ login(username: string, password: string): Observable<LoginResponse> {
25
+ return this.http.post<LoginResponse>('/api/login', { username, password })
26
+ .pipe(
27
+ tap(response => {
28
+ localStorage.setItem(this.tokenKey, response.token);
29
+ localStorage.setItem(this.usernameKey, response.username);
30
+ this.loggedInSubject.next(true);
31
+ })
32
+ );
33
+ }
34
+
35
+ logout(): void {
36
+ localStorage.removeItem(this.tokenKey);
37
+ localStorage.removeItem(this.usernameKey);
38
+ this.loggedInSubject.next(false);
39
+ this.router.navigate(['/login']);
40
+ }
41
+
42
+ getToken(): string | null {
43
+ return localStorage.getItem(this.tokenKey);
44
+ }
45
+
46
+ getUsername(): string | null {
47
+ return localStorage.getItem(this.usernameKey);
48
+ }
49
+
50
+ hasToken(): boolean {
51
+ return !!this.getToken();
52
+ }
53
+
54
+ isLoggedIn(): boolean {
55
+ return this.hasToken();
56
+ }
57
+ }
flare-ui/src/index.html ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Flare Administration</title>
6
+ <base href="/">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <link rel="icon" type="image/x-icon" href="favicon.ico">
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
10
+ </head>
11
+ <body>
12
+ <app-root></app-root>
13
+ </body>
14
+ </html>
flare-ui/src/main.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { bootstrapApplication } from '@angular/platform-browser';
2
+ import { provideRouter } from '@angular/router';
3
+ import { provideHttpClient, withInterceptors } from '@angular/common/http';
4
+ import { AppComponent } from './app/app.component';
5
+ import { routes } from './app/app.routes';
6
+ import { authInterceptor } from './app/interceptors/auth.interceptor';
7
+
8
+ bootstrapApplication(AppComponent, {
9
+ providers: [
10
+ provideRouter(routes),
11
+ provideHttpClient(
12
+ withInterceptors([authInterceptor])
13
+ )
14
+ ]
15
+ });
flare-ui/src/styles.scss ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Global Styles */
2
+ * {
3
+ box-sizing: border-box;
4
+ margin: 0;
5
+ padding: 0;
6
+ }
7
+
8
+ body {
9
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
10
+ font-size: 14px;
11
+ line-height: 1.5;
12
+ color: #333;
13
+ background-color: #f5f5f5;
14
+ }
15
+
16
+ /* Utility Classes */
17
+ .container {
18
+ max-width: 1200px;
19
+ margin: 0 auto;
20
+ padding: 0 20px;
21
+ }
22
+
23
+ .btn {
24
+ display: inline-block;
25
+ padding: 8px 16px;
26
+ font-size: 14px;
27
+ font-weight: 500;
28
+ text-align: center;
29
+ text-decoration: none;
30
+ border: none;
31
+ border-radius: 4px;
32
+ cursor: pointer;
33
+ transition: all 0.3s ease;
34
+
35
+ &.btn-primary {
36
+ background-color: #007bff;
37
+ color: white;
38
+
39
+ &:hover {
40
+ background-color: #0056b3;
41
+ }
42
+ }
43
+
44
+ &.btn-secondary {
45
+ background-color: #6c757d;
46
+ color: white;
47
+
48
+ &:hover {
49
+ background-color: #545b62;
50
+ }
51
+ }
52
+
53
+ &.btn-danger {
54
+ background-color: #dc3545;
55
+ color: white;
56
+
57
+ &:hover {
58
+ background-color: #c82333;
59
+ }
60
+ }
61
+
62
+ &:disabled {
63
+ opacity: 0.6;
64
+ cursor: not-allowed;
65
+ }
66
+ }
67
+
68
+ /* Form Styles */
69
+ .form-group {
70
+ margin-bottom: 1rem;
71
+
72
+ label {
73
+ display: block;
74
+ margin-bottom: 0.5rem;
75
+ font-weight: 500;
76
+ color: #495057;
77
+ }
78
+
79
+ input,
80
+ select,
81
+ textarea {
82
+ display: block;
83
+ width: 100%;
84
+ padding: 0.375rem 0.75rem;
85
+ font-size: 1rem;
86
+ line-height: 1.5;
87
+ color: #495057;
88
+ background-color: #fff;
89
+ background-clip: padding-box;
90
+ border: 1px solid #ced4da;
91
+ border-radius: 0.25rem;
92
+ transition: border-color 0.15s ease-in-out;
93
+
94
+ &:focus {
95
+ color: #495057;
96
+ background-color: #fff;
97
+ border-color: #80bdff;
98
+ outline: 0;
99
+ }
100
+ }
101
+
102
+ textarea {
103
+ min-height: 100px;
104
+ resize: vertical;
105
+ }
106
+ }
107
+
108
+ /* Table Styles */
109
+ .table {
110
+ width: 100%;
111
+ margin-bottom: 1rem;
112
+ background-color: white;
113
+ border-collapse: collapse;
114
+
115
+ th,
116
+ td {
117
+ padding: 0.75rem;
118
+ text-align: left;
119
+ border-bottom: 1px solid #dee2e6;
120
+ }
121
+
122
+ th {
123
+ background-color: #f8f9fa;
124
+ font-weight: 600;
125
+ color: #495057;
126
+ }
127
+
128
+ tbody tr:hover {
129
+ background-color: #f8f9fa;
130
+ }
131
+ }
132
+
133
+ /* Card Styles */
134
+ .card {
135
+ background-color: white;
136
+ border: 1px solid #dee2e6;
137
+ border-radius: 0.25rem;
138
+ box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
139
+ margin-bottom: 1rem;
140
+
141
+ .card-header {
142
+ padding: 0.75rem 1.25rem;
143
+ background-color: #f8f9fa;
144
+ border-bottom: 1px solid #dee2e6;
145
+ font-weight: 600;
146
+ }
147
+
148
+ .card-body {
149
+ padding: 1.25rem;
150
+ }
151
+ }
152
+
153
+ /* Tab Styles */
154
+ .tabs {
155
+ display: flex;
156
+ border-bottom: 2px solid #dee2e6;
157
+ margin-bottom: 1rem;
158
+
159
+ .tab {
160
+ padding: 0.5rem 1rem;
161
+ cursor: pointer;
162
+ border: none;
163
+ background: none;
164
+ font-weight: 500;
165
+ color: #6c757d;
166
+ transition: all 0.3s ease;
167
+
168
+ &.active {
169
+ color: #007bff;
170
+ border-bottom: 2px solid #007bff;
171
+ margin-bottom: -2px;
172
+ }
173
+
174
+ &:hover {
175
+ color: #007bff;
176
+ }
177
+ }
178
+ }
179
+
180
+ /* Alert Styles */
181
+ .alert {
182
+ padding: 0.75rem 1.25rem;
183
+ margin-bottom: 1rem;
184
+ border: 1px solid transparent;
185
+ border-radius: 0.25rem;
186
+
187
+ &.alert-danger {
188
+ color: #721c24;
189
+ background-color: #f8d7da;
190
+ border-color: #f5c6cb;
191
+ }
192
+
193
+ &.alert-success {
194
+ color: #155724;
195
+ background-color: #d4edda;
196
+ border-color: #c3e6cb;
197
+ }
198
+ }
199
+
200
+ /* Loading Spinner */
201
+ .spinner {
202
+ display: inline-block;
203
+ width: 20px;
204
+ height: 20px;
205
+ border: 3px solid rgba(0, 0, 0, 0.1);
206
+ border-radius: 50%;
207
+ border-top-color: #007bff;
208
+ animation: spin 1s ease-in-out infinite;
209
+ }
210
+
211
+ @keyframes spin {
212
+ to { transform: rotate(360deg); }
213
+ }
214
+
215
+ /* Dialog/Modal Styles */
216
+ .dialog-backdrop {
217
+ position: fixed;
218
+ top: 0;
219
+ left: 0;
220
+ width: 100%;
221
+ height: 100%;
222
+ background-color: rgba(0, 0, 0, 0.5);
223
+ display: flex;
224
+ align-items: center;
225
+ justify-content: center;
226
+ z-index: 1000;
227
+ }
228
+
229
+ .dialog {
230
+ background-color: white;
231
+ border-radius: 8px;
232
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
233
+ max-width: 600px;
234
+ width: 90%;
235
+ max-height: 90vh;
236
+ overflow: hidden;
237
+ display: flex;
238
+ flex-direction: column;
239
+
240
+ .dialog-header {
241
+ padding: 1rem 1.5rem;
242
+ border-bottom: 1px solid #dee2e6;
243
+ font-size: 1.25rem;
244
+ font-weight: 600;
245
+ }
246
+
247
+ .dialog-body {
248
+ padding: 1.5rem;
249
+ overflow-y: auto;
250
+ flex: 1;
251
+ }
252
+
253
+ .dialog-footer {
254
+ padding: 1rem 1.5rem;
255
+ border-top: 1px solid #dee2e6;
256
+ display: flex;
257
+ justify-content: flex-end;
258
+ gap: 0.5rem;
259
+ }
260
+ }
flare-ui/tsconfig.app.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./out-tsc/app",
5
+ "types": []
6
+ },
7
+ "files": [
8
+ "src/main.ts"
9
+ ],
10
+ "include": [
11
+ "src/**/*.d.ts"
12
+ ]
13
+ }
flare-ui/tsconfig.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compileOnSave": false,
3
+ "compilerOptions": {
4
+ "baseUrl": "./",
5
+ "outDir": "./dist/out-tsc",
6
+ "forceConsistentCasingInFileNames": true,
7
+ "strict": true,
8
+ "noImplicitOverride": true,
9
+ "noPropertyAccessFromIndexSignature": true,
10
+ "noImplicitReturns": true,
11
+ "noFallthroughCasesInSwitch": true,
12
+ "sourceMap": true,
13
+ "declaration": false,
14
+ "downlevelIteration": true,
15
+ "experimentalDecorators": true,
16
+ "moduleResolution": "node",
17
+ "importHelpers": true,
18
+ "target": "ES2022",
19
+ "module": "ES2022",
20
+ "useDefineForClassFields": false,
21
+ "lib": [
22
+ "ES2022",
23
+ "dom"
24
+ ]
25
+ },
26
+ "angularCompilerOptions": {
27
+ "enableI18nLegacyMessageIdFormat": false,
28
+ "strictInjectionParameters": true,
29
+ "strictInputAccessModifiers": true,
30
+ "strictTemplates": true
31
+ }
32
+ }