SCGR commited on
Commit
ba5edb0
Β·
1 Parent(s): 9355701

admin login

Browse files
frontend/src/App.tsx CHANGED
@@ -9,8 +9,10 @@ import ExplorePage from './pages/ExplorePage';
9
  import HelpPage from './pages/HelpPage';
10
  import MapDetailPage from './pages/MapDetailsPage';
11
  import DemoPage from './pages/DemoPage';
12
- import DevPage from './pages/DevPage';
 
13
  import { FilterProvider } from './contexts/FilterContext';
 
14
 
15
  const router = createHashRouter([
16
  {
@@ -22,7 +24,8 @@ const router = createHashRouter([
22
  { path: '/explore', element: <ExplorePage /> },
23
  { path: '/help', element: <HelpPage /> },
24
  { path: '/demo', element: <DemoPage /> },
25
- { path: '/dev', element: <DevPage /> },
 
26
  { path: '/map/:mapId', element: <MapDetailPage /> },
27
  ],
28
  },
@@ -95,9 +98,11 @@ function Application() {
95
  return (
96
  <AlertContext.Provider value={alertContextValue}>
97
  <LanguageContext.Provider value={languageContextValue}>
98
- <FilterProvider>
99
- <RouterProvider router={router} />
100
- </FilterProvider>
 
 
101
  </LanguageContext.Provider>
102
  </AlertContext.Provider>
103
  );
 
9
  import HelpPage from './pages/HelpPage';
10
  import MapDetailPage from './pages/MapDetailsPage';
11
  import DemoPage from './pages/DemoPage';
12
+
13
+ import AdminPage from './pages/AdminPage/AdminPage';
14
  import { FilterProvider } from './contexts/FilterContext';
15
+ import { AdminProvider } from './contexts/AdminContext';
16
 
17
  const router = createHashRouter([
18
  {
 
24
  { path: '/explore', element: <ExplorePage /> },
25
  { path: '/help', element: <HelpPage /> },
26
  { path: '/demo', element: <DemoPage /> },
27
+
28
+ { path: '/admin', element: <AdminPage /> },
29
  { path: '/map/:mapId', element: <MapDetailPage /> },
30
  ],
31
  },
 
98
  return (
99
  <AlertContext.Provider value={alertContextValue}>
100
  <LanguageContext.Provider value={languageContextValue}>
101
+ <AdminProvider>
102
+ <FilterProvider>
103
+ <RouterProvider router={router} />
104
+ </FilterProvider>
105
+ </AdminProvider>
106
  </LanguageContext.Provider>
107
  </AlertContext.Provider>
108
  );
frontend/src/components/HeaderNav.tsx CHANGED
@@ -8,6 +8,7 @@ import {
8
  GoMainIcon,
9
  SettingsIcon,
10
  } from "@ifrc-go/icons";
 
11
 
12
  declare global {
13
  interface Window {
@@ -19,12 +20,12 @@ const navItems = [
19
  { to: "/upload", label: "Upload", Icon: UploadCloudLineIcon },
20
  { to: "/explore", label: "Explore", Icon: SearchLineIcon },
21
  { to: "/analytics", label: "Analytics", Icon: AnalysisIcon },
22
- { to: "/dev", label: "Dev", Icon: SettingsIcon },
23
  ];
24
 
25
  export default function HeaderNav() {
26
  const location = useLocation();
27
  const navigate = useNavigate();
 
28
 
29
  return (
30
  <nav className="border-b border-gray-200 bg-white shadow-sm sticky top-0 z-50 backdrop-blur-sm bg-white/95">
@@ -72,18 +73,18 @@ export default function HeaderNav() {
72
  ? 'shadow-lg shadow-ifrcRed/20 transform scale-105'
73
  : 'hover:bg-white hover:shadow-md hover:scale-105'
74
  }`}
75
- onClick={() => {
76
- if (location.pathname === "/upload") {
77
- if (window.confirmNavigationIfNeeded) {
78
- window.confirmNavigationIfNeeded(to);
79
- return;
 
 
 
 
80
  }
81
- if (!confirm("You have unsaved changes. Are you sure you want to leave?")) {
82
- return;
83
- }
84
- }
85
- navigate(to);
86
- }}
87
  >
88
  <Icon className={`w-4 h-4 transition-transform duration-200 ${
89
  isActive ? 'scale-110' : 'group-hover:scale-110'
@@ -99,31 +100,66 @@ export default function HeaderNav() {
99
  })}
100
  </nav>
101
 
102
- <Button
103
- name="help"
104
- variant={location.pathname === '/help' ? "primary" : "tertiary"}
105
- size={1}
106
- className={`transition-all duration-200 ${
107
- location.pathname === '/help'
108
- ? 'shadow-lg shadow-ifrcRed/20 transform scale-105'
109
- : 'hover:bg-blue-50 hover:text-blue-600 hover:shadow-md hover:scale-105'
110
- }`}
111
- onClick={() => {
112
- if (location.pathname === "/upload") {
113
- if (window.confirmNavigationIfNeeded) {
114
- window.confirmNavigationIfNeeded('/help');
115
- return;
116
- }
117
- if (!confirm("You have unsaved changes. Are you sure you want to leave?")) {
118
- return;
 
 
119
  }
120
- }
121
- navigate('/help');
122
- }}
123
- >
124
- <QuestionLineIcon className="w-4 h-4" />
125
- <span className="inline ml-2 font-semibold">Help & Support</span>
126
- </Button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  </PageContainer>
128
  </nav>
129
  );
 
8
  GoMainIcon,
9
  SettingsIcon,
10
  } from "@ifrc-go/icons";
11
+ import { useAdmin } from "../contexts/AdminContext";
12
 
13
  declare global {
14
  interface Window {
 
20
  { to: "/upload", label: "Upload", Icon: UploadCloudLineIcon },
21
  { to: "/explore", label: "Explore", Icon: SearchLineIcon },
22
  { to: "/analytics", label: "Analytics", Icon: AnalysisIcon },
 
23
  ];
24
 
25
  export default function HeaderNav() {
26
  const location = useLocation();
27
  const navigate = useNavigate();
28
+ const { isAuthenticated } = useAdmin();
29
 
30
  return (
31
  <nav className="border-b border-gray-200 bg-white shadow-sm sticky top-0 z-50 backdrop-blur-sm bg-white/95">
 
73
  ? 'shadow-lg shadow-ifrcRed/20 transform scale-105'
74
  : 'hover:bg-white hover:shadow-md hover:scale-105'
75
  }`}
76
+ onClick={() => {
77
+ if (location.pathname === "/upload") {
78
+ if (window.confirmNavigationIfNeeded) {
79
+ window.confirmNavigationIfNeeded(to);
80
+ return;
81
+ }
82
+ if (!confirm("You have unsaved changes. Are you sure you want to leave?")) {
83
+ return;
84
+ }
85
  }
86
+ navigate(to);
87
+ }}
 
 
 
 
88
  >
89
  <Icon className={`w-4 h-4 transition-transform duration-200 ${
90
  isActive ? 'scale-110' : 'group-hover:scale-110'
 
100
  })}
101
  </nav>
102
 
103
+ <div className="flex items-center space-x-2">
104
+ <Button
105
+ name="help"
106
+ variant={location.pathname === '/help' ? "primary" : "tertiary"}
107
+ size={1}
108
+ className={`transition-all duration-200 ${
109
+ location.pathname === '/help'
110
+ ? 'shadow-lg shadow-ifrcRed/20 transform scale-105'
111
+ : 'hover:bg-blue-50 hover:text-blue-600 hover:shadow-md hover:scale-105'
112
+ }`}
113
+ onClick={() => {
114
+ if (location.pathname === "/upload") {
115
+ if (window.confirmNavigationIfNeeded) {
116
+ window.confirmNavigationIfNeeded('/help');
117
+ return;
118
+ }
119
+ if (!confirm("You have unsaved changes. Are you sure you want to leave?")) {
120
+ return;
121
+ }
122
  }
123
+ navigate('/help');
124
+ }}
125
+ >
126
+ <QuestionLineIcon className="w-4 h-4" />
127
+ <span className="inline ml-2 font-semibold">Help & Support</span>
128
+ </Button>
129
+
130
+ <div className="relative">
131
+ <Container withInternalPadding className="p-2">
132
+ <Button
133
+ name="dev"
134
+ variant={location.pathname === '/admin' ? "primary" : "tertiary"}
135
+ size={1}
136
+ className={`transition-all duration-200 ${
137
+ location.pathname === '/admin'
138
+ ? 'shadow-lg shadow-purple-500/20 transform scale-105'
139
+ : 'hover:bg-purple-50 hover:text-purple-600 hover:shadow-md hover:scale-105'
140
+ }`}
141
+ onClick={() => {
142
+ if (location.pathname === "/upload") {
143
+ if (window.confirmNavigationIfNeeded) {
144
+ window.confirmNavigationIfNeeded('/admin');
145
+ return;
146
+ }
147
+ if (!confirm("You have unsaved changes. Are you sure you want to leave?")) {
148
+ return;
149
+ }
150
+ }
151
+ navigate('/admin');
152
+ }}
153
+ >
154
+ <SettingsIcon className="w-4 h-4" />
155
+ <span className="inline ml-2 font-semibold">Dev</span>
156
+ </Button>
157
+ </Container>
158
+ {location.pathname === '/admin' && (
159
+ <div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 w-8 h-1 bg-purple-500 rounded-full animate-pulse"></div>
160
+ )}
161
+ </div>
162
+ </div>
163
  </PageContainer>
164
  </nav>
165
  );
frontend/src/contexts/AdminContext.tsx ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { createContext, useContext, useState, useEffect } from 'react';
2
+ import type { ReactNode } from 'react';
3
+
4
+ interface AdminContextType {
5
+ isAuthenticated: boolean;
6
+ isLoading: boolean;
7
+ login: (password: string) => Promise<boolean>;
8
+ logout: () => void;
9
+ verifyToken: () => Promise<void>;
10
+ }
11
+
12
+ const AdminContext = createContext<AdminContextType | undefined>(undefined);
13
+
14
+ export const useAdmin = () => {
15
+ const context = useContext(AdminContext);
16
+ if (context === undefined) {
17
+ throw new Error('useAdmin must be used within an AdminProvider');
18
+ }
19
+ return context;
20
+ };
21
+
22
+ interface AdminProviderProps {
23
+ children: ReactNode;
24
+ }
25
+
26
+ export const AdminProvider: React.FC<AdminProviderProps> = ({ children }) => {
27
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
28
+ const [isLoading, setIsLoading] = useState(true);
29
+
30
+ const verifyToken = async () => {
31
+ const adminToken = localStorage.getItem('adminToken');
32
+ if (!adminToken) {
33
+ setIsAuthenticated(false);
34
+ setIsLoading(false);
35
+ return;
36
+ }
37
+
38
+ try {
39
+ const response = await fetch('/api/admin/verify', {
40
+ method: 'POST',
41
+ headers: {
42
+ 'Content-Type': 'application/json',
43
+ 'Authorization': `Bearer ${adminToken}`
44
+ }
45
+ });
46
+
47
+ if (response.ok) {
48
+ setIsAuthenticated(true);
49
+ } else {
50
+ // Token is invalid, remove it
51
+ localStorage.removeItem('adminToken');
52
+ setIsAuthenticated(false);
53
+ }
54
+ } catch (error) {
55
+ console.error('Error verifying admin token:', error);
56
+ localStorage.removeItem('adminToken');
57
+ setIsAuthenticated(false);
58
+ } finally {
59
+ setIsLoading(false);
60
+ }
61
+ };
62
+
63
+ const login = async (password: string): Promise<boolean> => {
64
+ try {
65
+ const response = await fetch('/api/admin/login', {
66
+ method: 'POST',
67
+ headers: {
68
+ 'Content-Type': 'application/json',
69
+ },
70
+ body: JSON.stringify({ password })
71
+ });
72
+
73
+ if (response.ok) {
74
+ const data = await response.json();
75
+ localStorage.setItem('adminToken', data.access_token);
76
+ setIsAuthenticated(true);
77
+ return true;
78
+ } else {
79
+ return false;
80
+ }
81
+ } catch (error) {
82
+ console.error('Login error:', error);
83
+ return false;
84
+ }
85
+ };
86
+
87
+ const logout = () => {
88
+ localStorage.removeItem('adminToken');
89
+ setIsAuthenticated(false);
90
+ };
91
+
92
+ // Check authentication status on mount
93
+ useEffect(() => {
94
+ verifyToken();
95
+ }, []);
96
+
97
+ const value: AdminContextType = {
98
+ isAuthenticated,
99
+ isLoading,
100
+ login,
101
+ logout,
102
+ verifyToken
103
+ };
104
+
105
+ return (
106
+ <AdminContext.Provider value={value}>
107
+ {children}
108
+ </AdminContext.Provider>
109
+ );
110
+ };
frontend/src/pages/{DevPage.tsx β†’ AdminPage/AdminPage.tsx} RENAMED
@@ -1,11 +1,17 @@
1
- import { useState, useEffect } from 'react';
2
- import {
3
- PageContainer, Heading, Button, Container,
4
- } from '@ifrc-go/ui';
5
 
6
  const SELECTED_MODEL_KEY = 'selectedVlmModel';
7
 
8
- export default function DevPage() {
 
 
 
 
 
 
9
  const [availableModels, setAvailableModels] = useState<Array<{
10
  m_code: string;
11
  label: string;
@@ -15,8 +21,10 @@ export default function DevPage() {
15
  const [selectedModel, setSelectedModel] = useState<string>('');
16
 
17
  useEffect(() => {
18
- fetchModels();
19
- }, []);
 
 
20
 
21
  const fetchModels = () => {
22
  fetch('/api/models')
@@ -26,7 +34,10 @@ export default function DevPage() {
26
 
27
  const persistedModel = localStorage.getItem(SELECTED_MODEL_KEY);
28
  if (modelsData.models && modelsData.models.length > 0) {
29
- if (persistedModel && modelsData.models.find((m: { m_code: string; is_available: boolean }) => m.m_code === persistedModel && m.is_available)) {
 
 
 
30
  setSelectedModel(persistedModel);
31
  } else {
32
  const firstAvailableModel = modelsData.models.find((m: { is_available: boolean }) => m.is_available) || modelsData.models[0];
@@ -36,7 +47,7 @@ export default function DevPage() {
36
  }
37
  })
38
  .catch(() => {
39
-
40
  });
41
  };
42
 
@@ -71,13 +82,117 @@ export default function DevPage() {
71
 
72
  const handleModelChange = (modelCode: string) => {
73
  setSelectedModel(modelCode);
74
- localStorage.setItem(SELECTED_MODEL_KEY, modelCode);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  };
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  return (
78
  <PageContainer>
79
  <div className="mx-auto max-w-screen-lg px-4 sm:px-6 lg:px-8 py-6 sm:py-10">
80
- <Heading level={1}>Development & Testing</Heading>
 
 
 
 
 
 
 
 
81
 
82
  <div className="mt-8 space-y-8">
83
  {/* Model Selection Section */}
@@ -99,6 +214,7 @@ export default function DevPage() {
99
  onChange={(e) => handleModelChange(e.target.value)}
100
  className="border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ifrcRed focus:border-transparent min-w-[200px]"
101
  >
 
102
  {availableModels
103
  .filter(model => model.is_available)
104
  .map(model => (
@@ -109,12 +225,13 @@ export default function DevPage() {
109
  </select>
110
  {selectedModel && (
111
  <span className="text-xs text-green-600 bg-green-50 px-2 py-1 rounded">
112
- βœ“ Active for caption generation
 
 
 
113
  </span>
114
  )}
115
  </div>
116
-
117
-
118
  </div>
119
  </Container>
120
 
@@ -146,16 +263,16 @@ export default function DevPage() {
146
  <td className="px-4 py-2 text-sm font-mono">{model.m_code}</td>
147
  <td className="px-4 py-2 text-sm">{model.label}</td>
148
  <td className="px-4 py-2 text-sm">{model.model_type}</td>
149
- <td className="px-4 py-2 text-sm">
150
- <Button
151
- name={`toggle-${model.m_code}`}
152
- variant={model.is_available ? "primary" : "secondary"}
153
- size={1}
154
- onClick={() => toggleModelAvailability(model.m_code, model.is_available)}
155
- >
156
- {model.is_available ? 'Enabled' : 'Disabled'}
157
- </Button>
158
- </td>
159
  </tr>
160
  ))}
161
  </tbody>
@@ -164,65 +281,125 @@ export default function DevPage() {
164
  </div>
165
  </Container>
166
 
167
- {/* API Testing Section */}
168
- <Container
169
- heading="API Testing"
170
- headingLevel={2}
171
- withHeaderBorder
172
- withInternalPadding
173
- >
174
- <div className="space-y-4">
175
- <p className="text-gray-700">
176
- Test API endpoints and model functionality.
177
- </p>
178
-
179
- <div className="flex flex-wrap gap-4">
180
- <Button
181
- name="test-models-api"
182
- variant="secondary"
183
- onClick={() => {
184
- fetch('/api/models')
185
- .then(r => r.json())
186
- .then(() => {
187
- alert('Models API response received successfully');
188
- })
189
- .catch(() => {
190
- alert('Models API error occurred');
191
- });
192
- }}
193
- >
194
- Test Models API
195
- </Button>
196
-
197
- <Button
198
- name="test-selected-model"
199
- variant="secondary"
200
- disabled={!selectedModel}
201
- onClick={() => {
202
- if (!selectedModel) return;
203
- fetch(`/api/models/${selectedModel}/test`)
204
- .then(r => r.json())
205
- .then(() => {
206
- alert('Model test completed successfully');
207
- })
208
- .catch(() => {
209
- alert('Model test failed');
210
- });
211
- }}
212
- >
213
- Test Selected Model
214
- </Button>
 
 
 
 
 
 
 
 
 
 
 
215
 
216
- <Button
217
- name="refresh-models"
218
- variant="secondary"
219
- onClick={fetchModels}
220
- >
221
- Refresh Models
222
- </Button>
223
- </div>
224
- </div>
225
- </Container>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  </div>
227
  </div>
228
  </PageContainer>
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { useAdmin } from '../../contexts/AdminContext';
4
+ import { PageContainer, Heading, Button, Container, TextInput } from '@ifrc-go/ui';
5
 
6
  const SELECTED_MODEL_KEY = 'selectedVlmModel';
7
 
8
+ export default function AdminPage() {
9
+ const navigate = useNavigate();
10
+ const { isAuthenticated, isLoading, login, logout } = useAdmin();
11
+ const [password, setPassword] = useState('');
12
+ const [error, setError] = useState('');
13
+ const [isLoggingIn, setIsLoggingIn] = useState(false);
14
+
15
  const [availableModels, setAvailableModels] = useState<Array<{
16
  m_code: string;
17
  label: string;
 
21
  const [selectedModel, setSelectedModel] = useState<string>('');
22
 
23
  useEffect(() => {
24
+ if (isAuthenticated) {
25
+ fetchModels();
26
+ }
27
+ }, [isAuthenticated]);
28
 
29
  const fetchModels = () => {
30
  fetch('/api/models')
 
34
 
35
  const persistedModel = localStorage.getItem(SELECTED_MODEL_KEY);
36
  if (modelsData.models && modelsData.models.length > 0) {
37
+ if (persistedModel === 'random') {
38
+ // Keep random selection
39
+ setSelectedModel('random');
40
+ } else if (persistedModel && modelsData.models.find((m: { m_code: string; is_available: boolean }) => m.m_code === persistedModel && m.is_available)) {
41
  setSelectedModel(persistedModel);
42
  } else {
43
  const firstAvailableModel = modelsData.models.find((m: { is_available: boolean }) => m.is_available) || modelsData.models[0];
 
47
  }
48
  })
49
  .catch(() => {
50
+ // Handle error silently
51
  });
52
  };
53
 
 
82
 
83
  const handleModelChange = (modelCode: string) => {
84
  setSelectedModel(modelCode);
85
+ if (modelCode === 'random') {
86
+ // For random selection, we'll select a random available model when needed
87
+ localStorage.setItem(SELECTED_MODEL_KEY, 'random');
88
+ } else {
89
+ localStorage.setItem(SELECTED_MODEL_KEY, modelCode);
90
+ }
91
+ };
92
+
93
+ const handleLogin = async (e: React.FormEvent) => {
94
+ e.preventDefault();
95
+ if (!password.trim()) {
96
+ setError('Please enter a password');
97
+ return;
98
+ }
99
+
100
+ setIsLoggingIn(true);
101
+ setError('');
102
+
103
+ try {
104
+ const success = await login(password);
105
+ if (!success) {
106
+ setError('Invalid password');
107
+ }
108
+ } catch (err) {
109
+ setError('Login failed. Please try again.');
110
+ } finally {
111
+ setIsLoggingIn(false);
112
+ }
113
+ };
114
+
115
+ const handleLogout = () => {
116
+ logout();
117
+ setPassword('');
118
+ setError('');
119
  };
120
 
121
+ if (isLoading) {
122
+ return (
123
+ <PageContainer>
124
+ <div className="flex items-center justify-center min-h-[400px]">
125
+ <div className="text-center">
126
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-ifrcRed mx-auto mb-4"></div>
127
+ <p className="text-gray-600">Loading...</p>
128
+ </div>
129
+ </div>
130
+ </PageContainer>
131
+ );
132
+ }
133
+
134
+ if (!isAuthenticated) {
135
+ return (
136
+ <PageContainer>
137
+ <div className="mx-auto max-w-md px-4 sm:px-6 lg:px-8 py-6 sm:py-10">
138
+ <div className="text-center mb-8">
139
+ <Heading level={1}>Admin Login</Heading>
140
+ </div>
141
+
142
+ <form onSubmit={handleLogin} className="space-y-6">
143
+ <div>
144
+ <label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
145
+ Password
146
+ </label>
147
+ <TextInput
148
+ id="password"
149
+ name="password"
150
+ type="password"
151
+ value={password}
152
+ onChange={(value) => setPassword(value || '')}
153
+ placeholder="Enter admin password"
154
+ required
155
+ className="w-full"
156
+ />
157
+ </div>
158
+
159
+ {error && (
160
+ <div className="bg-ifrcRed/10 border border-ifrcRed/20 rounded-md p-3">
161
+ <p className="text-sm text-ifrcRed font-medium">{error}</p>
162
+ </div>
163
+ )}
164
+
165
+ <div className="flex justify-center">
166
+ <Container withInternalPadding className="p-2">
167
+ <Button
168
+ name="login"
169
+ type="submit"
170
+ variant="primary"
171
+ size={2}
172
+ disabled={isLoggingIn}
173
+ >
174
+ {isLoggingIn ? 'Logging in...' : 'Login'}
175
+ </Button>
176
+ </Container>
177
+ </div>
178
+ </form>
179
+ </div>
180
+ </PageContainer>
181
+ );
182
+ }
183
+
184
  return (
185
  <PageContainer>
186
  <div className="mx-auto max-w-screen-lg px-4 sm:px-6 lg:px-8 py-6 sm:py-10">
187
+ <div className="flex justify-end mb-8">
188
+ <Button
189
+ name="logout"
190
+ variant="secondary"
191
+ onClick={handleLogout}
192
+ >
193
+ Logout
194
+ </Button>
195
+ </div>
196
 
197
  <div className="mt-8 space-y-8">
198
  {/* Model Selection Section */}
 
214
  onChange={(e) => handleModelChange(e.target.value)}
215
  className="border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ifrcRed focus:border-transparent min-w-[200px]"
216
  >
217
+ <option value="random">Random</option>
218
  {availableModels
219
  .filter(model => model.is_available)
220
  .map(model => (
 
225
  </select>
226
  {selectedModel && (
227
  <span className="text-xs text-green-600 bg-green-50 px-2 py-1 rounded">
228
+ {selectedModel === 'random'
229
+ ? 'βœ“ Random model selection active'
230
+ : 'βœ“ Active for caption generation'
231
+ }
232
  </span>
233
  )}
234
  </div>
 
 
235
  </div>
236
  </Container>
237
 
 
263
  <td className="px-4 py-2 text-sm font-mono">{model.m_code}</td>
264
  <td className="px-4 py-2 text-sm">{model.label}</td>
265
  <td className="px-4 py-2 text-sm">{model.model_type}</td>
266
+ <td className="px-4 py-2 text-sm">
267
+ <Button
268
+ name={`toggle-${model.m_code}`}
269
+ variant={model.is_available ? "primary" : "secondary"}
270
+ size={1}
271
+ onClick={() => toggleModelAvailability(model.m_code, model.is_available)}
272
+ >
273
+ {model.is_available ? 'Enabled' : 'Disabled'}
274
+ </Button>
275
+ </td>
276
  </tr>
277
  ))}
278
  </tbody>
 
281
  </div>
282
  </Container>
283
 
284
+ {/* API Testing Section */}
285
+ <Container
286
+ heading="API Testing"
287
+ headingLevel={2}
288
+ withHeaderBorder
289
+ withInternalPadding
290
+ >
291
+ <div className="space-y-4">
292
+ <p className="text-gray-700">
293
+ Test API endpoints and model functionality.
294
+ </p>
295
+
296
+ <div className="flex flex-wrap gap-4">
297
+ <Button
298
+ name="test-models-api"
299
+ variant="secondary"
300
+ onClick={() => {
301
+ fetch('/api/models')
302
+ .then(r => r.json())
303
+ .then(() => {
304
+ alert('Models API response received successfully');
305
+ })
306
+ .catch(() => {
307
+ alert('Models API error occurred');
308
+ });
309
+ }}
310
+ >
311
+ Test Models API
312
+ </Button>
313
+
314
+ <Button
315
+ name="test-selected-model"
316
+ variant="secondary"
317
+ disabled={!selectedModel}
318
+ onClick={() => {
319
+ if (!selectedModel) return;
320
+ fetch(`/api/models/${selectedModel}/test`)
321
+ .then(r => r.json())
322
+ .then(() => {
323
+ alert('Model test completed successfully');
324
+ })
325
+ .catch(() => {
326
+ alert('Model test failed');
327
+ });
328
+ }}
329
+ >
330
+ Test Selected Model
331
+ </Button>
332
+
333
+ <Button
334
+ name="refresh-models"
335
+ variant="secondary"
336
+ onClick={fetchModels}
337
+ >
338
+ Refresh Models
339
+ </Button>
340
+ </div>
341
+ </div>
342
+ </Container>
343
 
344
+ {/* Schema Validation Section */}
345
+ <Container
346
+ heading="Schema Validation"
347
+ headingLevel={2}
348
+ withHeaderBorder
349
+ withInternalPadding
350
+ >
351
+ <div className="space-y-4">
352
+ <p className="text-gray-700">
353
+ Monitor and test JSON schema validation for VLM responses.
354
+ </p>
355
+
356
+ <div className="flex flex-wrap gap-4">
357
+ <Button
358
+ name="view-schemas"
359
+ variant="secondary"
360
+ onClick={() => {
361
+ fetch('/api/schemas', {
362
+ headers: {
363
+ 'Authorization': `Bearer ${localStorage.getItem('adminToken')}`
364
+ }
365
+ })
366
+ .then(r => r.json())
367
+ .then(data => {
368
+ console.log('Schemas:', data);
369
+ alert(`Found ${data.length} schemas. Check console for details.`);
370
+ })
371
+ .catch(() => {
372
+ alert('Failed to fetch schemas');
373
+ });
374
+ }}
375
+ >
376
+ View Schemas
377
+ </Button>
378
+
379
+ <Button
380
+ name="validation-stats"
381
+ variant="secondary"
382
+ onClick={() => {
383
+ fetch('/api/schemas/validation-stats', {
384
+ headers: {
385
+ 'Authorization': `Bearer ${localStorage.getItem('adminToken')}`
386
+ }
387
+ })
388
+ .then(r => r.json())
389
+ .then(data => {
390
+ console.log('Validation Stats:', data);
391
+ alert(`Validation: ${data.validation_passed} passed, ${data.validation_failed} failed. Check console for details.`);
392
+ })
393
+ .catch(() => {
394
+ alert('Failed to fetch validation stats');
395
+ });
396
+ }}
397
+ >
398
+ Validation Stats
399
+ </Button>
400
+ </div>
401
+ </div>
402
+ </Container>
403
  </div>
404
  </div>
405
  </PageContainer>
frontend/src/pages/UploadPage/UploadPage.tsx CHANGED
@@ -342,7 +342,7 @@ export default function UploadPage() {
342
 
343
  const modelName = localStorage.getItem(SELECTED_MODEL_KEY);
344
  if (modelName) {
345
- fd.append('model_name', modelName);
346
  }
347
 
348
  try {
@@ -365,7 +365,7 @@ export default function UploadPage() {
365
  body: new URLSearchParams({
366
  title: title || 'Generated Caption',
367
  prompt: imageType === 'drone_image' ? 'DEFAULT_DRONE_IMAGE' : 'DEFAULT_CRISIS_MAP',
368
- ...(modelName && { model_name: modelName })
369
  })
370
  },
371
  );
@@ -449,11 +449,11 @@ export default function UploadPage() {
449
  const capRes = await fetch(`/api/images/${newId}/caption`, {
450
  method: 'POST',
451
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
452
- body: new URLSearchParams({
453
- title: 'Generated Caption',
454
- prompt: imageType === 'drone_image' ? 'DEFAULT_DRONE_IMAGE' : 'DEFAULT_CRISIS_MAP',
455
- ...(modelName && { model_name: modelName }),
456
- }),
457
  });
458
  const capJson = await readJsonSafely(capRes);
459
  if (!capRes.ok) throw new Error((capJson.error as string) || 'Caption failed');
 
342
 
343
  const modelName = localStorage.getItem(SELECTED_MODEL_KEY);
344
  if (modelName) {
345
+ fd.append('model_name', modelName);
346
  }
347
 
348
  try {
 
365
  body: new URLSearchParams({
366
  title: title || 'Generated Caption',
367
  prompt: imageType === 'drone_image' ? 'DEFAULT_DRONE_IMAGE' : 'DEFAULT_CRISIS_MAP',
368
+ ...(modelName && { model_name: modelName })
369
  })
370
  },
371
  );
 
449
  const capRes = await fetch(`/api/images/${newId}/caption`, {
450
  method: 'POST',
451
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
452
+ body: new URLSearchParams({
453
+ title: 'Generated Caption',
454
+ prompt: imageType === 'drone_image' ? 'DEFAULT_DRONE_IMAGE' : 'DEFAULT_CRISIS_MAP',
455
+ ...(modelName && { model_name: modelName }),
456
+ }),
457
  });
458
  const capJson = await readJsonSafely(capRes);
459
  if (!capRes.ok) throw new Error((capJson.error as string) || 'Caption failed');
py_backend/ADMIN_SETUP.md ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Admin Authentication Setup
2
+
3
+ This document explains how to set up admin authentication for the PromptAid Vision application.
4
+
5
+ ## Environment Variables
6
+
7
+ Add these environment variables to your `.env` file or Hugging Face Space secrets:
8
+
9
+ ### Required Variables
10
+
11
+ ```bash
12
+ # Admin password for authentication
13
+ ADMIN_PASSWORD=your-secure-admin-password-here
14
+
15
+ # JWT secret key for token signing (use a strong, random key)
16
+ JWT_SECRET_KEY=your-secure-jwt-secret-key-here
17
+ ```
18
+
19
+ ### Optional Variables
20
+
21
+ ```bash
22
+ # Database connection
23
+ DATABASE_URL=postgresql://username:password@localhost:5432/database_name
24
+
25
+ # Storage configuration
26
+ STORAGE_PROVIDER=local
27
+ STORAGE_DIR=./uploads
28
+ ```
29
+
30
+ ## How It Works
31
+
32
+ ### 1. Admin Login
33
+ - Users click "Admin Login" in the header navigation
34
+ - They enter the admin password
35
+ - If correct, they receive a JWT token valid for 24 hours
36
+
37
+ ### 2. Authentication Flow
38
+ - Frontend stores the JWT token in localStorage
39
+ - Token is sent with each admin API request in Authorization header
40
+ - Backend verifies token validity and role
41
+
42
+ ### 3. Security Features
43
+ - JWT tokens expire after 24 hours
44
+ - Tokens are verified on each admin request
45
+ - Password is stored in environment variables (not in code)
46
+
47
+ ## API Endpoints
48
+
49
+ ### POST `/api/admin/login`
50
+ - **Purpose**: Authenticate admin user
51
+ - **Body**: `{"password": "admin_password"}`
52
+ - **Response**: `{"token": "jwt_token", "expires_at": "timestamp"}`
53
+
54
+ ### POST `/api/admin/verify`
55
+ - **Purpose**: Verify admin token
56
+ - **Headers**: `Authorization: Bearer <token>`
57
+ - **Response**: `{"valid": true/false, "message": "..."}`
58
+
59
+ ### GET `/api/admin/status`
60
+ - **Purpose**: Get admin status (protected endpoint)
61
+ - **Headers**: `Authorization: Bearer <token>`
62
+ - **Response**: `{"status": "authenticated", "role": "admin", "timestamp": "..."}`
63
+
64
+ ## Development vs Production
65
+
66
+ ### Development
67
+ - Default password: `admin123`
68
+ - Default JWT secret: `your-secret-key-change-in-production`
69
+ - **⚠️ Change these in production!**
70
+
71
+ ### Production
72
+ - Use strong, random passwords
73
+ - Use secure JWT secret keys
74
+ - Store secrets in environment variables or Hugging Face Space secrets
75
+ - Consider implementing password hashing for additional security
76
+
77
+ ## Future Enhancements
78
+
79
+ - User-specific accounts and permissions
80
+ - Role-based access control
81
+ - Password hashing with bcrypt
82
+ - Session management
83
+ - Audit logging
84
+ - Two-factor authentication
85
+
86
+ ## Troubleshooting
87
+
88
+ ### Common Issues
89
+
90
+ 1. **"Invalid admin password"**
91
+ - Check that `ADMIN_PASSWORD` environment variable is set correctly
92
+ - Ensure no extra spaces or characters
93
+
94
+ 2. **"Token is invalid or expired"**
95
+ - Token may have expired (24-hour limit)
96
+ - Try logging in again
97
+ - Check `JWT_SECRET_KEY` is consistent
98
+
99
+ 3. **"Method Not Allowed"**
100
+ - Ensure admin router is properly included in main.py
101
+ - Check API endpoint URLs are correct
102
+
103
+ ### Debug Steps
104
+
105
+ 1. Verify environment variables are loaded
106
+ 2. Check backend logs for authentication errors
107
+ 3. Verify JWT token format in browser localStorage
108
+ 4. Test API endpoints directly with tools like curl or Postman
py_backend/alembic/versions/{0002_drone_pose_fields_and_schema.py β†’ 0002_drone_fields.py} RENAMED
@@ -10,7 +10,7 @@ import json
10
 
11
  # Alembic identifiers
12
  revision = "0002_drone_fields"
13
- down_revision = "0001_initial_schema_and_seed"
14
  branch_labels = None
15
  depends_on = None
16
 
 
10
 
11
  # Alembic identifiers
12
  revision = "0002_drone_fields"
13
+ down_revision = "b8fc40bfe3c7"
14
  branch_labels = None
15
  depends_on = None
16
 
py_backend/alembic/versions/0003_fix_json_schemas.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fix JSON schemas to match validation requirements
2
+
3
+ Revision ID: 0003_fix_json_schemas
4
+ Revises: 0002_drone_pose_fields_and_schema
5
+ Create Date: 2025-01-20 10:00:00.000000
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+ import json
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = '0003_fix_json_schemas'
14
+ down_revision = '0002_drone_fields'
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade():
20
+ """Fix the JSON schemas to match validation requirements"""
21
+
22
+ # Fix the default crisis map schema
23
+ crisis_schema = {
24
+ "type": "object",
25
+ "properties": {
26
+ "analysis": {"type": "string"},
27
+ "metadata": {
28
+ "type": "object",
29
+ "properties": {
30
+ "title": {"type": "string"},
31
+ "source": {"type": "string"},
32
+ "type": {"type": "string"},
33
+ "countries": {"type": "array", "items": {"type": "string"}},
34
+ "epsg": {"type": "string"}
35
+ },
36
+ "required": ["title", "source", "type", "countries", "epsg"]
37
+ }
38
+ },
39
+ "required": ["analysis", "metadata"]
40
+ }
41
+
42
+ op.execute(
43
+ sa.text(
44
+ """
45
+ UPDATE json_schemas
46
+ SET schema = CAST(:schema AS JSONB)
47
+ WHERE schema_id = '[email protected]'
48
+ """
49
+ ).bindparams(
50
+ schema=json.dumps(crisis_schema, separators=(",", ":"))
51
+ )
52
+ )
53
+
54
+ # Fix the drone schema (the current one is mostly correct, but let's ensure it's perfect)
55
+ drone_schema = {
56
+ "type": "object",
57
+ "properties": {
58
+ "analysis": {"type": "string"},
59
+ "metadata": {
60
+ "type": "object",
61
+ "properties": {
62
+ "title": {"type": ["string", "null"]},
63
+ "source": {"type": ["string", "null"]},
64
+ "type": {"type": ["string", "null"]},
65
+ "countries": {"type": ["array", "null"], "items": {"type": "string"}},
66
+ "epsg": {"type": ["string", "null"]},
67
+ "center_lat": {"type": ["number", "null"], "minimum": -90, "maximum": 90},
68
+ "center_lon": {"type": ["number", "null"], "minimum": -180, "maximum": 180},
69
+ "amsl_m": {"type": ["number", "null"]},
70
+ "agl_m": {"type": ["number", "null"]},
71
+ "heading_deg": {"type": ["number", "null"], "minimum": 0, "maximum": 360},
72
+ "yaw_deg": {"type": ["number", "null"], "minimum": -180, "maximum": 180},
73
+ "pitch_deg": {"type": ["number", "null"], "minimum": -90, "maximum": 90},
74
+ "roll_deg": {"type": ["number", "null"], "minimum": -180, "maximum": 180},
75
+ "rtk_fix": {"type": ["boolean", "null"]},
76
+ "std_h_m": {"type": ["number", "null"], "minimum": 0},
77
+ "std_v_m": {"type": ["number", "null"], "minimum": 0}
78
+ }
79
+ }
80
+ },
81
+ "required": ["analysis", "metadata"]
82
+ }
83
+
84
+ op.execute(
85
+ sa.text(
86
+ """
87
+ UPDATE json_schemas
88
+ SET schema = CAST(:schema AS JSONB)
89
+ WHERE schema_id = '[email protected]'
90
+ """
91
+ ).bindparams(
92
+ schema=json.dumps(drone_schema, separators=(",", ":"))
93
+ )
94
+ )
95
+
96
+ print("βœ“ Updated JSON schemas to match validation requirements")
97
+
98
+
99
+ def downgrade():
100
+ """Revert to previous schema versions"""
101
+
102
+ # Revert crisis map schema to original
103
+ original_crisis_schema = {
104
+ "type": "object",
105
+ "properties": {
106
+ "analysis": {"type": "string"},
107
+ "metadata": {
108
+ "type": "object",
109
+ "properties": {
110
+ "title": {"type": "string"},
111
+ "source": {"type": "string"},
112
+ "type": {"type": "string"},
113
+ "countries": {"type": "array", "items": {"type": "string"}},
114
+ "epsg": {"type": "string"}
115
+ }
116
+ }
117
+ },
118
+ "required": ["analysis", "metadata"]
119
+ }
120
+
121
+ op.execute(
122
+ sa.text(
123
+ """
124
+ UPDATE json_schemas
125
+ SET schema = CAST(:schema AS JSONB)
126
+ WHERE schema_id = '[email protected]'
127
+ """
128
+ ).bindparams(
129
+ schema=json.dumps(original_crisis_schema, separators=(",", ":"))
130
+ )
131
+ )
132
+
133
+ # Revert drone schema to original
134
+ original_drone_schema = {
135
+ "type": "object",
136
+ "properties": {
137
+ "analysis": {"type": "string"},
138
+ "metadata": {
139
+ "type": "object",
140
+ "properties": {
141
+ "title": {"type": ["string", "null"]},
142
+ "source": {"type": ["string", "null"]},
143
+ "type": {"type": ["string", "null"]},
144
+ "countries": {"type": ["array", "null"], "items": {"type": "string"}},
145
+ "epsg": {"type": ["string", "null"]},
146
+ "center_lat": {"type": ["number", "null"], "minimum": -90, "maximum": 90},
147
+ "center_lon": {"type": ["number", "null"], "minimum": -180, "maximum": 180},
148
+ "amsl_m": {"type": ["number", "null"]},
149
+ "agl_m": {"type": ["number", "null"]},
150
+ "heading_deg": {"type": ["number", "null"], "minimum": 0, "maximum": 360},
151
+ "yaw_deg": {"type": ["number", "null"], "minimum": -180, "maximum": 180},
152
+ "pitch_deg": {"type": ["number", "null"], "minimum": -90, "maximum": 90},
153
+ "roll_deg": {"type": ["number", "null"], "minimum": -180, "maximum": 180},
154
+ "rtk_fix": {"type": ["boolean", "null"]},
155
+ "std_h_m": {"type": ["number", "null"], "minimum": 0},
156
+ "std_v_m": {"type": ["number", "null"], "minimum": 0}
157
+ }
158
+ }
159
+ },
160
+ "required": ["analysis", "metadata"]
161
+ }
162
+
163
+ op.execute(
164
+ sa.text(
165
+ """
166
+ UPDATE json_schemas
167
+ SET schema = CAST(:schema AS JSONB)
168
+ WHERE schema_id = '[email protected]'
169
+ """
170
+ ).bindparams(
171
+ schema=json.dumps(original_drone_schema, separators=(",", ":"))
172
+ )
173
+ )
174
+
175
+ print("βœ“ Reverted JSON schemas to previous versions")
py_backend/alembic/versions/b8fc40bfe3c7_initial_schema_seed.py CHANGED
@@ -9,7 +9,7 @@ import sqlalchemy as sa
9
  from sqlalchemy.dialects import postgresql
10
  import pycountry
11
 
12
- revision = '0001_initial_schema_and_seed'
13
  down_revision = None
14
  branch_labels = None
15
  depends_on = None
 
9
  from sqlalchemy.dialects import postgresql
10
  import pycountry
11
 
12
+ revision = 'b8fc40bfe3c7'
13
  down_revision = None
14
  branch_labels = None
15
  depends_on = None
py_backend/app/crud.py CHANGED
@@ -211,3 +211,15 @@ def get_models(db: Session):
211
  def get_model(db: Session, m_code: str):
212
  """Get a specific model by code"""
213
  return db.get(models.Models, m_code)
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  def get_model(db: Session, m_code: str):
212
  """Get a specific model by code"""
213
  return db.get(models.Models, m_code)
214
+
215
+ def get_all_schemas(db: Session):
216
+ """Get all JSON schemas"""
217
+ return db.query(models.JSONSchema).all()
218
+
219
+ def get_schema(db: Session, schema_id: str):
220
+ """Get a specific JSON schema by ID"""
221
+ return db.query(models.JSONSchema).filter(models.JSONSchema.schema_id == schema_id).first()
222
+
223
+ def get_recent_images_with_validation(db: Session, limit: int = 100):
224
+ """Get recent images with validation info"""
225
+ return db.query(models.Images).order_by(models.Images.created_at.desc()).limit(limit).all()
py_backend/app/main.py CHANGED
@@ -7,10 +7,15 @@ from fastapi.middleware.cors import CORSMiddleware
7
  from fastapi.staticfiles import StaticFiles
8
  from fastapi.responses import FileResponse, JSONResponse, HTMLResponse
9
 
 
 
 
10
  from app.config import settings
11
  from app.routers import upload, caption, metadata, models
12
  from app.routers.images import router as images_router
13
  from app.routers.prompts import router as prompts_router
 
 
14
 
15
  app = FastAPI(title="PromptAid Vision")
16
 
@@ -29,6 +34,8 @@ app.include_router(models.router, prefix="/api", tags=["models"]
29
  app.include_router(upload.router, prefix="/api/images", tags=["images"])
30
  app.include_router(images_router, prefix="/api/contribute", tags=["contribute"])
31
  app.include_router(prompts_router, prefix="/api/prompts", tags=["prompts"])
 
 
32
 
33
  @app.get("/health", include_in_schema=False, response_class=JSONResponse)
34
  async def health():
 
7
  from fastapi.staticfiles import StaticFiles
8
  from fastapi.responses import FileResponse, JSONResponse, HTMLResponse
9
 
10
+ from dotenv import load_dotenv
11
+ load_dotenv()
12
+
13
  from app.config import settings
14
  from app.routers import upload, caption, metadata, models
15
  from app.routers.images import router as images_router
16
  from app.routers.prompts import router as prompts_router
17
+ from app.routers.admin import router as admin_router
18
+ from app.routers.schemas import router as schemas_router
19
 
20
  app = FastAPI(title="PromptAid Vision")
21
 
 
34
  app.include_router(upload.router, prefix="/api/images", tags=["images"])
35
  app.include_router(images_router, prefix="/api/contribute", tags=["contribute"])
36
  app.include_router(prompts_router, prefix="/api/prompts", tags=["prompts"])
37
+ app.include_router(admin_router, prefix="/api/admin", tags=["admin"])
38
+ app.include_router(schemas_router, prefix="/api", tags=["schemas"])
39
 
40
  @app.get("/health", include_in_schema=False, response_class=JSONResponse)
41
  async def health():
py_backend/app/routers/admin.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import jwt
3
+ import hashlib
4
+ from datetime import datetime, timedelta
5
+ from fastapi import APIRouter, HTTPException, Depends, Header
6
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
7
+ from pydantic import BaseModel
8
+ from sqlalchemy.orm import Session
9
+
10
+ from ..database import SessionLocal
11
+ from ..config import settings
12
+
13
+ router = APIRouter()
14
+ security = HTTPBearer()
15
+
16
+ # Models
17
+ class AdminLoginRequest(BaseModel):
18
+ password: str
19
+
20
+ class AdminLoginResponse(BaseModel):
21
+ access_token: str
22
+ token_type: str = "bearer"
23
+ expires_at: str
24
+
25
+ class AdminVerifyResponse(BaseModel):
26
+ valid: bool
27
+ message: str
28
+
29
+ def get_db():
30
+ db = SessionLocal()
31
+ try:
32
+ yield db
33
+ finally:
34
+ db.close()
35
+
36
+ def get_admin_password():
37
+ """Get admin password from environment variable"""
38
+ password = os.getenv('ADMIN_PASSWORD')
39
+ if not password:
40
+ raise HTTPException(
41
+ status_code=500,
42
+ detail="ADMIN_PASSWORD environment variable not set"
43
+ )
44
+ return password
45
+
46
+ def create_admin_token():
47
+ """Create a JWT token for admin authentication"""
48
+ # In production, use a proper secret key
49
+ secret_key = os.getenv('JWT_SECRET_KEY', 'your-secret-key-change-in-production')
50
+
51
+ payload = {
52
+ 'role': 'admin',
53
+ 'exp': datetime.utcnow() + timedelta(hours=24), # 24 hour expiry
54
+ 'iat': datetime.utcnow()
55
+ }
56
+
57
+ token = jwt.encode(payload, secret_key, algorithm='HS256')
58
+ return token
59
+
60
+ def verify_admin_token(token: str):
61
+ """Verify the admin JWT token"""
62
+ try:
63
+ secret_key = os.getenv('JWT_SECRET_KEY', 'your-secret-key-change-in-production')
64
+ payload = jwt.decode(token, secret_key, algorithms=['HS256'])
65
+
66
+ if payload.get('role') != 'admin':
67
+ return False
68
+
69
+ # Check if token is expired
70
+ exp = payload.get('exp')
71
+ if exp and datetime.utcnow() > datetime.fromtimestamp(exp):
72
+ return False
73
+
74
+ return True
75
+ except jwt.ExpiredSignatureError:
76
+ return False
77
+ except jwt.InvalidTokenError:
78
+ return False
79
+
80
+ @router.post("/login", response_model=AdminLoginResponse)
81
+ async def admin_login(request: AdminLoginRequest):
82
+ """Admin login endpoint"""
83
+ admin_password = get_admin_password()
84
+
85
+ # Hash the provided password and compare with stored hash
86
+ # For now, using simple comparison (in production, use proper hashing)
87
+ if request.password == admin_password:
88
+ token = create_admin_token()
89
+ expires_at = datetime.utcnow() + timedelta(hours=24)
90
+
91
+ return AdminLoginResponse(
92
+ access_token=token,
93
+ expires_at=expires_at.isoformat()
94
+ )
95
+ else:
96
+ raise HTTPException(
97
+ status_code=401,
98
+ detail="Invalid admin password"
99
+ )
100
+
101
+ @router.post("/verify", response_model=AdminVerifyResponse)
102
+ async def verify_admin(credentials: HTTPAuthorizationCredentials = Depends(security)):
103
+ """Verify admin token endpoint"""
104
+ token = credentials.credentials
105
+
106
+ if verify_admin_token(token):
107
+ return AdminVerifyResponse(
108
+ valid=True,
109
+ message="Token is valid"
110
+ )
111
+ else:
112
+ return AdminVerifyResponse(
113
+ valid=False,
114
+ message="Token is invalid or expired"
115
+ )
116
+
117
+ @router.get("/status")
118
+ async def admin_status(credentials: HTTPAuthorizationCredentials = Depends(security)):
119
+ """Get admin status (protected endpoint)"""
120
+ token = credentials.credentials
121
+
122
+ if not verify_admin_token(token):
123
+ raise HTTPException(
124
+ status_code=401,
125
+ detail="Invalid or expired token"
126
+ )
127
+
128
+ return {
129
+ "status": "authenticated",
130
+ "role": "admin",
131
+ "timestamp": datetime.utcnow().isoformat()
132
+ }
py_backend/app/routers/caption.py CHANGED
@@ -3,6 +3,7 @@ from sqlalchemy.orm import Session
3
  from typing import List
4
  from .. import crud, database, schemas, storage
5
  from ..services.vlm_service import vlm_manager
 
6
  from ..config import settings
7
 
8
  from ..services.stub_vlm_service import StubVLMService
@@ -132,11 +133,34 @@ async def create_caption(
132
  prompt=prompt_text,
133
  metadata_instructions=metadata_instructions,
134
  model_name=model_name,
 
135
  )
136
- text = result.get("caption", "")
137
- used_model = model_name or "STUB_MODEL"
138
  raw = result.get("raw_response", {})
139
- metadata = result.get("metadata", {})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  except Exception as e:
141
  print(f"VLM error, using fallback: {e}")
142
  text = "This is a fallback caption due to VLM service error."
 
3
  from typing import List
4
  from .. import crud, database, schemas, storage
5
  from ..services.vlm_service import vlm_manager
6
+ from ..services.schema_validator import schema_validator
7
  from ..config import settings
8
 
9
  from ..services.stub_vlm_service import StubVLMService
 
133
  prompt=prompt_text,
134
  metadata_instructions=metadata_instructions,
135
  model_name=model_name,
136
+ db_session=db,
137
  )
138
+
139
+ # Get the raw response for validation
140
  raw = result.get("raw_response", {})
141
+
142
+ # Validate and clean the data using schema validation
143
+ image_type = img.image_type
144
+ print(f"DEBUG: Validating data for image type: {image_type}")
145
+ print(f"DEBUG: Raw data structure: {list(raw.keys()) if isinstance(raw, dict) else 'Not a dict'}")
146
+
147
+ cleaned_data, is_valid, validation_error = schema_validator.clean_and_validate_data(raw, image_type)
148
+
149
+ if is_valid:
150
+ print(f"βœ“ Schema validation passed for {image_type}")
151
+ text = cleaned_data.get("analysis", "")
152
+ metadata = cleaned_data.get("metadata", {})
153
+ else:
154
+ print(f"⚠ Schema validation failed for {image_type}: {validation_error}")
155
+ # Use fallback but log the validation error
156
+ text = result.get("caption", "This is a fallback caption due to schema validation error.")
157
+ metadata = result.get("metadata", {})
158
+ raw["validation_error"] = validation_error
159
+ raw["validation_failed"] = True
160
+
161
+ # Use the actual model that was used, not the requested model_name
162
+ used_model = result.get("model", model_name) or "STUB_MODEL"
163
+
164
  except Exception as e:
165
  print(f"VLM error, using fallback: {e}")
166
  text = "This is a fallback caption due to VLM service error."
py_backend/app/routers/models.py CHANGED
@@ -29,7 +29,17 @@ def get_available_models(db: Session = Depends(get_db)):
29
  "config": model.config
30
  })
31
 
32
- return {"models": models_info}
 
 
 
 
 
 
 
 
 
 
33
  except Exception as e:
34
  raise HTTPException(500, f"Failed to get models: {str(e)}")
35
 
 
29
  "config": model.config
30
  })
31
 
32
+ # Add debug info about registered services
33
+ registered_services = list(vlm_manager.services.keys())
34
+
35
+ return {
36
+ "models": models_info,
37
+ "debug": {
38
+ "registered_services": registered_services,
39
+ "total_services": len(registered_services),
40
+ "available_db_models": [m.m_code for m in db_models if m.is_available]
41
+ }
42
+ }
43
  except Exception as e:
44
  raise HTTPException(500, f"Failed to get models: {str(e)}")
45
 
py_backend/app/routers/schemas.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, Depends
2
+ from sqlalchemy.orm import Session
3
+ from typing import List, Dict, Any
4
+ from .. import crud, database, schemas
5
+ from ..services.schema_validator import schema_validator
6
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
7
+ from ..routers.admin import verify_admin_token
8
+
9
+ router = APIRouter()
10
+ security = HTTPBearer()
11
+
12
+ def get_db():
13
+ db = database.SessionLocal()
14
+ try:
15
+ yield db
16
+ finally:
17
+ db.close()
18
+
19
+ def verify_admin_access(credentials: HTTPAuthorizationCredentials = Depends(security)):
20
+ """Verify admin token for schema endpoints"""
21
+ token = credentials.credentials
22
+
23
+ if not verify_admin_token(token):
24
+ raise HTTPException(
25
+ status_code=401,
26
+ detail="Invalid or expired admin token"
27
+ )
28
+
29
+ return token
30
+
31
+ @router.get("/schemas", response_model=List[Dict[str, Any]])
32
+ async def get_schemas(
33
+ db: Session = Depends(get_db),
34
+ token: str = Depends(verify_admin_access)
35
+ ):
36
+ """Get all JSON schemas (admin only)"""
37
+ try:
38
+ # Get schemas from database
39
+ db_schemas = crud.get_all_schemas(db)
40
+
41
+ schemas_list = []
42
+ for schema in db_schemas:
43
+ schemas_list.append({
44
+ "schema_id": schema.schema_id,
45
+ "title": schema.title,
46
+ "version": schema.version,
47
+ "created_at": schema.created_at.isoformat() if schema.created_at else None,
48
+ "schema": schema.schema
49
+ })
50
+
51
+ return schemas_list
52
+ except Exception as e:
53
+ raise HTTPException(500, f"Failed to get schemas: {str(e)}")
54
+
55
+ @router.get("/schemas/{schema_id}", response_model=Dict[str, Any])
56
+ async def get_schema(
57
+ schema_id: str,
58
+ db: Session = Depends(get_db),
59
+ token: str = Depends(verify_admin_access)
60
+ ):
61
+ """Get a specific JSON schema (admin only)"""
62
+ try:
63
+ schema = crud.get_schema(db, schema_id)
64
+ if not schema:
65
+ raise HTTPException(404, f"Schema {schema_id} not found")
66
+
67
+ return {
68
+ "schema_id": schema.schema_id,
69
+ "title": schema.title,
70
+ "version": schema.version,
71
+ "created_at": schema.created_at.isoformat() if schema.created_at else None,
72
+ "schema": schema.schema
73
+ }
74
+ except HTTPException:
75
+ raise
76
+ except Exception as e:
77
+ raise HTTPException(500, f"Failed to get schema: {str(e)}")
78
+
79
+ @router.post("/schemas/validate")
80
+ async def validate_data_against_schema(
81
+ data: Dict[str, Any],
82
+ schema_id: str,
83
+ db: Session = Depends(get_db),
84
+ token: str = Depends(verify_admin_access)
85
+ ):
86
+ """Validate JSON data against a specific schema (admin only)"""
87
+ try:
88
+ # Get the schema from database
89
+ schema = crud.get_schema(db, schema_id)
90
+ if not schema:
91
+ raise HTTPException(404, f"Schema {schema_id} not found")
92
+
93
+ # Validate the data
94
+ is_valid, error_msg = schema_validator.validate_against_schema(
95
+ data, schema.schema, schema_id
96
+ )
97
+
98
+ return {
99
+ "is_valid": is_valid,
100
+ "error_message": error_msg,
101
+ "schema_id": schema_id,
102
+ "data": data
103
+ }
104
+ except HTTPException:
105
+ raise
106
+ except Exception as e:
107
+ raise HTTPException(500, f"Validation failed: {str(e)}")
108
+
109
+ @router.post("/schemas/test-validation")
110
+ async def test_schema_validation(
111
+ image_type: str,
112
+ data: Dict[str, Any],
113
+ token: str = Depends(verify_admin_access)
114
+ ):
115
+ """Test data validation against image type schemas (admin only)"""
116
+ try:
117
+ # Validate data against the appropriate schema for the image type
118
+ cleaned_data, is_valid, error_msg = schema_validator.clean_and_validate_data(
119
+ data, image_type
120
+ )
121
+
122
+ return {
123
+ "is_valid": is_valid,
124
+ "error_message": error_msg,
125
+ "image_type": image_type,
126
+ "original_data": data,
127
+ "cleaned_data": cleaned_data if is_valid else None
128
+ }
129
+ except Exception as e:
130
+ raise HTTPException(500, f"Test validation failed: {str(e)}")
131
+
132
+ @router.get("/schemas/validation-stats")
133
+ async def get_validation_stats(
134
+ db: Session = Depends(get_db),
135
+ token: str = Depends(verify_admin_access)
136
+ ):
137
+ """Get validation statistics (admin only)"""
138
+ try:
139
+ # Get recent images with validation info
140
+ recent_images = crud.get_recent_images_with_validation(db, limit=100)
141
+
142
+ stats = {
143
+ "total_images": len(recent_images),
144
+ "validation_passed": 0,
145
+ "validation_failed": 0,
146
+ "validation_errors": [],
147
+ "by_image_type": {
148
+ "crisis_map": {"total": 0, "passed": 0, "failed": 0},
149
+ "drone_image": {"total": 0, "passed": 0, "failed": 0}
150
+ }
151
+ }
152
+
153
+ for img in recent_images:
154
+ if hasattr(img, 'raw_json') and img.raw_json:
155
+ image_type = img.image_type
156
+ if image_type in stats["by_image_type"]:
157
+ stats["by_image_type"][image_type]["total"] += 1
158
+
159
+ # Check if validation failed
160
+ if img.raw_json.get("validation_failed"):
161
+ stats["validation_failed"] += 1
162
+ if image_type in stats["by_image_type"]:
163
+ stats["by_image_type"][image_type]["failed"] += 1
164
+
165
+ error = img.raw_json.get("validation_error", "Unknown error")
166
+ stats["validation_errors"].append({
167
+ "image_id": str(img.image_id),
168
+ "image_type": image_type,
169
+ "error": error,
170
+ "created_at": img.created_at.isoformat() if img.created_at else None
171
+ })
172
+ else:
173
+ stats["validation_passed"] += 1
174
+ if image_type in stats["by_image_type"]:
175
+ stats["by_image_type"][image_type]["passed"] += 1
176
+
177
+ return stats
178
+ except Exception as e:
179
+ raise HTTPException(500, f"Failed to get validation stats: {str(e)}")
py_backend/app/services/huggingface_service.py CHANGED
@@ -152,6 +152,6 @@ class InstructBLIPService(HuggingFaceService):
152
  """InstructBLIP model service using Hugging Face"""
153
 
154
  def __init__(self, api_key: str):
155
- super().__init__(api_key, "microsoft/git-base")
156
  self.model_name = "INSTRUCTBLIP_VICUNA_7B"
157
  self.model_type = ModelType.CUSTOM
 
152
  """InstructBLIP model service using Hugging Face"""
153
 
154
  def __init__(self, api_key: str):
155
+ super().__init__(api_key, "Salesforce/instructblip-vicuna-7b")
156
  self.model_name = "INSTRUCTBLIP_VICUNA_7B"
157
  self.model_type = ModelType.CUSTOM
py_backend/app/services/schema_validator.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import Dict, Any, Optional, Tuple
3
+ from jsonschema import validate, ValidationError
4
+ from jsonschema.validators import Draft7Validator
5
+ import logging
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class SchemaValidator:
10
+ """Service for validating JSON data against stored schemas"""
11
+
12
+ def __init__(self):
13
+ self.validators = {}
14
+
15
+ def validate_against_schema(self, data: Dict[str, Any], schema: Dict[str, Any], schema_id: str) -> Tuple[bool, Optional[str]]:
16
+ """
17
+ Validate JSON data against a schema
18
+
19
+ Args:
20
+ data: The JSON data to validate
21
+ schema: The JSON schema to validate against
22
+ schema_id: Identifier for the schema (for logging)
23
+
24
+ Returns:
25
+ Tuple of (is_valid, error_message)
26
+ """
27
+ try:
28
+ # Use Draft7Validator for better error messages
29
+ validator = Draft7Validator(schema)
30
+ errors = list(validator.iter_errors(data))
31
+
32
+ if errors:
33
+ error_messages = []
34
+ for error in errors:
35
+ path = " -> ".join(str(p) for p in error.path) if error.path else "root"
36
+ error_messages.append(f"{path}: {error.message}")
37
+
38
+ error_msg = f"Schema validation failed for {schema_id}: {'; '.join(error_messages)}"
39
+ logger.warning(error_msg)
40
+ return False, error_msg
41
+
42
+ logger.info(f"Schema validation passed for {schema_id}")
43
+ return True, None
44
+
45
+ except Exception as e:
46
+ error_msg = f"Schema validation error for {schema_id}: {str(e)}"
47
+ logger.error(error_msg)
48
+ return False, error_msg
49
+
50
+ def validate_crisis_map_data(self, data: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
51
+ """
52
+ Validate crisis map data against the default schema
53
+ """
54
+ # Define the expected crisis map schema
55
+ crisis_schema = {
56
+ "type": "object",
57
+ "properties": {
58
+ "analysis": {"type": "string"},
59
+ "metadata": {
60
+ "type": "object",
61
+ "properties": {
62
+ "title": {"type": "string"},
63
+ "source": {"type": "string"},
64
+ "type": {"type": "string"},
65
+ "countries": {"type": "array", "items": {"type": "string"}},
66
+ "epsg": {"type": "string"}
67
+ },
68
+ "required": ["title", "source", "type", "countries", "epsg"]
69
+ }
70
+ },
71
+ "required": ["analysis", "metadata"]
72
+ }
73
+
74
+ return self.validate_against_schema(data, crisis_schema, "crisis_map")
75
+
76
+ def validate_drone_data(self, data: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
77
+ """
78
+ Validate drone data against the drone schema
79
+ """
80
+ # Define the expected drone schema
81
+ drone_schema = {
82
+ "type": "object",
83
+ "properties": {
84
+ "analysis": {"type": "string"},
85
+ "metadata": {
86
+ "type": "object",
87
+ "properties": {
88
+ "title": {"type": ["string", "null"]},
89
+ "source": {"type": ["string", "null"]},
90
+ "type": {"type": ["string", "null"]},
91
+ "countries": {"type": ["array", "null"], "items": {"type": "string"}},
92
+ "epsg": {"type": ["string", "null"]},
93
+ "center_lat": {"type": ["number", "null"], "minimum": -90, "maximum": 90},
94
+ "center_lon": {"type": ["number", "null"], "minimum": -180, "maximum": 180},
95
+ "amsl_m": {"type": ["number", "null"]},
96
+ "agl_m": {"type": ["number", "null"]},
97
+ "heading_deg": {"type": ["number", "null"], "minimum": 0, "maximum": 360},
98
+ "yaw_deg": {"type": ["number", "null"], "minimum": -180, "maximum": 180},
99
+ "pitch_deg": {"type": ["number", "null"], "minimum": -90, "maximum": 90},
100
+ "roll_deg": {"type": ["number", "null"], "minimum": -180, "maximum": 180},
101
+ "rtk_fix": {"type": ["boolean", "null"]},
102
+ "std_h_m": {"type": ["number", "null"], "minimum": 0},
103
+ "std_v_m": {"type": ["number", "null"], "minimum": 0}
104
+ }
105
+ }
106
+ },
107
+ "required": ["analysis", "metadata"]
108
+ }
109
+
110
+ return self.validate_against_schema(data, drone_schema, "drone")
111
+
112
+ def validate_data_by_type(self, data: Dict[str, Any], image_type: str) -> Tuple[bool, Optional[str]]:
113
+ """
114
+ Validate data based on image type
115
+
116
+ Args:
117
+ data: The JSON data to validate
118
+ image_type: Either 'crisis_map' or 'drone_image'
119
+
120
+ Returns:
121
+ Tuple of (is_valid, error_message)
122
+ """
123
+ if image_type == 'drone_image':
124
+ return self.validate_drone_data(data)
125
+ elif image_type == 'crisis_map':
126
+ return self.validate_crisis_map_data(data)
127
+ else:
128
+ return False, f"Unknown image type: {image_type}"
129
+
130
+ def clean_and_validate_data(self, raw_data: Dict[str, Any], image_type: str) -> Tuple[Dict[str, Any], bool, Optional[str]]:
131
+ """
132
+ Clean and validate data, returning cleaned data, validation status, and any errors
133
+
134
+ Args:
135
+ raw_data: Raw data from VLM
136
+ image_type: Type of image being processed
137
+
138
+ Returns:
139
+ Tuple of (cleaned_data, is_valid, error_message)
140
+ """
141
+ try:
142
+ # Extract the main content (handle different VLM response formats)
143
+ if "content" in raw_data:
144
+ # Some VLM models wrap content in a "content" field
145
+ content = raw_data["content"]
146
+ if isinstance(content, str):
147
+ # Try to parse JSON from string content
148
+ try:
149
+ parsed_content = json.loads(content)
150
+ data = parsed_content
151
+ except json.JSONDecodeError:
152
+ # If it's not JSON, treat as analysis
153
+ data = {"analysis": content, "metadata": {}}
154
+ else:
155
+ data = content
156
+ else:
157
+ data = raw_data
158
+
159
+ # Validate the data
160
+ is_valid, error_msg = self.validate_data_by_type(data, image_type)
161
+
162
+ if is_valid:
163
+ # Clean the data (remove any extra fields, normalize)
164
+ cleaned_data = self._clean_data(data, image_type)
165
+ return cleaned_data, True, None
166
+ else:
167
+ return data, False, error_msg
168
+
169
+ except Exception as e:
170
+ error_msg = f"Data processing error: {str(e)}"
171
+ logger.error(error_msg)
172
+ return raw_data, False, error_msg
173
+
174
+ def _clean_data(self, data: Dict[str, Any], image_type: str) -> Dict[str, Any]:
175
+ """
176
+ Clean and normalize the data structure
177
+ """
178
+ cleaned = {
179
+ "analysis": data.get("analysis", ""),
180
+ "metadata": {}
181
+ }
182
+
183
+ metadata = data.get("metadata", {})
184
+
185
+ # Clean metadata based on image type
186
+ if image_type == 'crisis_map':
187
+ cleaned["metadata"] = {
188
+ "title": metadata.get("title", ""),
189
+ "source": metadata.get("source", "OTHER"),
190
+ "type": metadata.get("type", "OTHER"),
191
+ "countries": metadata.get("countries", []),
192
+ "epsg": metadata.get("epsg", "OTHER")
193
+ }
194
+ elif image_type == 'drone_image':
195
+ cleaned["metadata"] = {
196
+ "title": metadata.get("title"),
197
+ "source": metadata.get("source"),
198
+ "type": metadata.get("type"),
199
+ "countries": metadata.get("countries"),
200
+ "epsg": metadata.get("epsg"),
201
+ "center_lat": metadata.get("center_lat"),
202
+ "center_lon": metadata.get("center_lon"),
203
+ "amsl_m": metadata.get("amsl_m"),
204
+ "agl_m": metadata.get("agl_m"),
205
+ "heading_deg": metadata.get("heading_deg"),
206
+ "yaw_deg": metadata.get("yaw_deg"),
207
+ "pitch_deg": metadata.get("pitch_deg"),
208
+ "roll_deg": metadata.get("roll_deg"),
209
+ "rtk_fix": metadata.get("rtk_fix"),
210
+ "std_h_m": metadata.get("std_h_m"),
211
+ "std_v_m": metadata.get("std_v_m")
212
+ }
213
+
214
+ return cleaned
215
+
216
+ # Global instance
217
+ schema_validator = SchemaValidator()
py_backend/app/services/vlm_service.py CHANGED
@@ -62,18 +62,65 @@ class VLMServiceManager:
62
  """Get list of available model names"""
63
  return list(self.services.keys())
64
 
65
- async def generate_caption(self, image_bytes: bytes, prompt: str, metadata_instructions: str = "", model_name: str | None = None) -> dict:
66
  """Generate caption using the specified model or fallback to available service."""
67
 
68
  service = None
69
- if model_name:
70
  service = self.services.get(model_name)
71
  if not service:
72
  print(f"Model '{model_name}' not found, using fallback")
73
 
74
  if not service and self.services:
75
- service = next(iter(self.services.values()))
76
- print(f"Using fallback service: {service.model_name}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
  if not service:
79
  raise ValueError("No VLM services available")
 
62
  """Get list of available model names"""
63
  return list(self.services.keys())
64
 
65
+ async def generate_caption(self, image_bytes: bytes, prompt: str, metadata_instructions: str = "", model_name: str | None = None, db_session = None) -> dict:
66
  """Generate caption using the specified model or fallback to available service."""
67
 
68
  service = None
69
+ if model_name and model_name != "random":
70
  service = self.services.get(model_name)
71
  if not service:
72
  print(f"Model '{model_name}' not found, using fallback")
73
 
74
  if not service and self.services:
75
+ # If random is selected or no specific model, choose a random available service
76
+ if db_session:
77
+ # Check database availability for random selection
78
+ try:
79
+ from .. import crud
80
+ available_models = crud.get_models(db_session)
81
+ available_model_codes = [m.m_code for m in available_models if m.is_available]
82
+
83
+ print(f"DEBUG: Available models in database: {available_model_codes}")
84
+ print(f"DEBUG: Registered services: {list(self.services.keys())}")
85
+
86
+ # Filter services to only those marked as available in database
87
+ available_services = [s for s in self.services.values() if s.model_name in available_model_codes]
88
+
89
+ print(f"DEBUG: Available services after filtering: {[s.model_name for s in available_services]}")
90
+
91
+ if available_services:
92
+ import random
93
+ import time
94
+ # Use current time as seed for better randomness
95
+ random.seed(int(time.time() * 1000000) % 1000000)
96
+
97
+ # Shuffle the list first for better randomization
98
+ shuffled_services = available_services.copy()
99
+ random.shuffle(shuffled_services)
100
+
101
+ service = shuffled_services[0]
102
+ print(f"Randomly selected service: {service.model_name} (from {len(available_services)} available)")
103
+ print(f"DEBUG: All available services were: {[s.model_name for s in available_services]}")
104
+ print(f"DEBUG: Shuffled order: {[s.model_name for s in shuffled_services]}")
105
+ else:
106
+ # Fallback to any service
107
+ service = next(iter(self.services.values()))
108
+ print(f"Using fallback service: {service.model_name}")
109
+ except Exception as e:
110
+ print(f"Error checking database availability: {e}, using fallback")
111
+ service = next(iter(self.services.values()))
112
+ print(f"Using fallback service: {service.model_name}")
113
+ else:
114
+ # No database session, use service property
115
+ available_services = [s for s in self.services.values() if s.is_available]
116
+ if available_services:
117
+ import random
118
+ service = random.choice(available_services)
119
+ print(f"Randomly selected service: {service.model_name}")
120
+ else:
121
+ # Fallback to any service
122
+ service = next(iter(self.services.values()))
123
+ print(f"Using fallback service: {service.model_name}")
124
 
125
  if not service:
126
  raise ValueError("No VLM services available")
py_backend/browser_test.js ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Test schema endpoints in browser console
2
+ // Copy and paste this into your browser's developer console
3
+
4
+ async function testSchemaEndpoints() {
5
+ console.log("πŸ§ͺ Testing Schema Endpoints...");
6
+
7
+ // Get the admin token from localStorage
8
+ const token = localStorage.getItem('adminToken');
9
+
10
+ if (!token) {
11
+ console.log("❌ No admin token found. Please login first.");
12
+ return;
13
+ }
14
+
15
+ console.log("βœ… Admin token found:", token.substring(0, 20) + "...");
16
+
17
+ const headers = {
18
+ 'Authorization': `Bearer ${token}`,
19
+ 'Content-Type': 'application/json'
20
+ };
21
+
22
+ try {
23
+ // Test get schemas
24
+ console.log("\n1. Testing GET /api/schemas...");
25
+ const schemasResponse = await fetch('/api/schemas', { headers });
26
+ console.log("Response status:", schemasResponse.status);
27
+
28
+ if (schemasResponse.ok) {
29
+ const schemas = await schemasResponse.json();
30
+ console.log("βœ… Schemas found:", schemas.length);
31
+ schemas.forEach(schema => {
32
+ console.log(` - ${schema.schema_id}: ${schema.title}`);
33
+ });
34
+ } else {
35
+ const errorText = await schemasResponse.text();
36
+ console.log("❌ Error:", errorText);
37
+ }
38
+
39
+ // Test validation stats
40
+ console.log("\n2. Testing GET /api/schemas/validation-stats...");
41
+ const statsResponse = await fetch('/api/schemas/validation-stats', { headers });
42
+ console.log("Response status:", statsResponse.status);
43
+
44
+ if (statsResponse.ok) {
45
+ const stats = await statsResponse.json();
46
+ console.log("βœ… Validation stats:", stats);
47
+ } else {
48
+ const errorText = await statsResponse.text();
49
+ console.log("❌ Error:", errorText);
50
+ }
51
+
52
+ } catch (error) {
53
+ console.log("❌ Network error:", error);
54
+ }
55
+ }
56
+
57
+ // Run the test
58
+ testSchemaEndpoints();
py_backend/requirements.txt CHANGED
@@ -20,3 +20,5 @@ requests
20
  pycountry>=22.3.5
21
  pycountry-convert>=0.7.2
22
  python-multipart
 
 
 
20
  pycountry>=22.3.5
21
  pycountry-convert>=0.7.2
22
  python-multipart
23
+ PyJWT
24
+ jsonschema
py_backend/test_admin_endpoints.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test admin endpoints and schema validation
4
+ """
5
+
6
+ import requests
7
+ import json
8
+ import os
9
+
10
+ # Update this to your backend URL
11
+ BASE_URL = "http://localhost:8000" # Change if different
12
+
13
+ def test_admin_login():
14
+ """Test admin login"""
15
+ print("πŸ§ͺ Testing Admin Login...")
16
+
17
+ # You'll need to set your admin password here or in environment
18
+ password = os.getenv("ADMIN_PASSWORD", "your_password_here")
19
+
20
+ try:
21
+ response = requests.post(f"{BASE_URL}/api/admin/login",
22
+ json={"password": password})
23
+
24
+ if response.status_code == 200:
25
+ data = response.json()
26
+ print(f"βœ… Admin login successful")
27
+ print(f" Token type: {data.get('token_type', 'N/A')}")
28
+ print(f" Expires at: {data.get('expires_at', 'N/A')}")
29
+ return data.get('access_token')
30
+ else:
31
+ print(f"❌ Admin login failed: {response.status_code}")
32
+ print(f" Response: {response.text}")
33
+ return None
34
+
35
+ except Exception as e:
36
+ print(f"❌ Admin login error: {e}")
37
+ return None
38
+
39
+ def test_schema_endpoints(token):
40
+ """Test schema endpoints with token"""
41
+ if not token:
42
+ print("❌ No token available, skipping schema tests")
43
+ return
44
+
45
+ print("\nπŸ§ͺ Testing Schema Endpoints...")
46
+
47
+ headers = {"Authorization": f"Bearer {token}"}
48
+
49
+ # Test get schemas
50
+ try:
51
+ response = requests.get(f"{BASE_URL}/api/schemas", headers=headers)
52
+ if response.status_code == 200:
53
+ schemas = response.json()
54
+ print(f"βœ… Get schemas successful: found {len(schemas)} schemas")
55
+ for schema in schemas:
56
+ print(f" - {schema['schema_id']}: {schema['title']}")
57
+ else:
58
+ print(f"❌ Get schemas failed: {response.status_code}")
59
+ print(f" Response: {response.text}")
60
+ except Exception as e:
61
+ print(f"❌ Get schemas error: {e}")
62
+
63
+ # Test validation stats
64
+ try:
65
+ response = requests.get(f"{BASE_URL}/api/schemas/validation-stats", headers=headers)
66
+ if response.status_code == 200:
67
+ stats = response.json()
68
+ print(f"βœ… Validation stats successful:")
69
+ print(f" Total images: {stats.get('total_images', 0)}")
70
+ print(f" Validation passed: {stats.get('validation_passed', 0)}")
71
+ print(f" Validation failed: {stats.get('validation_failed', 0)}")
72
+ else:
73
+ print(f"❌ Validation stats failed: {response.status_code}")
74
+ print(f" Response: {response.text}")
75
+ except Exception as e:
76
+ print(f"❌ Validation stats error: {e}")
77
+
78
+ def main():
79
+ print("πŸš€ Testing Admin and Schema Endpoints")
80
+ print("=" * 50)
81
+
82
+ # Test admin login
83
+ token = test_admin_login()
84
+
85
+ # Test schema endpoints
86
+ test_schema_endpoints(token)
87
+
88
+ print(f"\nπŸ“‹ Instructions:")
89
+ print(f"1. Make sure your backend is running")
90
+ print(f"2. Set ADMIN_PASSWORD environment variable or update the password in this script")
91
+ print(f"3. Update BASE_URL if your backend is not on localhost:8000")
92
+
93
+ if __name__ == "__main__":
94
+ main()
py_backend/test_schema_integration.py ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Comprehensive test script for schema validation integration
4
+ """
5
+
6
+ import requests
7
+ import json
8
+ import sys
9
+ import os
10
+
11
+ BASE_URL = "http://localhost:8000"
12
+
13
+ def test_schema_endpoints():
14
+ """Test admin schema management endpoints"""
15
+ print("πŸ§ͺ Testing Schema Management Endpoints...")
16
+
17
+ # First, login to get admin token
18
+ login_data = {"password": os.getenv("ADMIN_PASSWORD", "your_password_here")}
19
+
20
+ try:
21
+ login_response = requests.post(f"{BASE_URL}/api/admin/login", json=login_data)
22
+ if login_response.status_code != 200:
23
+ print(f"❌ Admin login failed: {login_response.status_code}")
24
+ print("Please set ADMIN_PASSWORD environment variable or update the password in the script")
25
+ return False
26
+
27
+ token = login_response.json().get("access_token")
28
+ headers = {"Authorization": f"Bearer {token}"}
29
+
30
+ # Test getting all schemas
31
+ schemas_response = requests.get(f"{BASE_URL}/api/schemas", headers=headers)
32
+ if schemas_response.status_code == 200:
33
+ schemas = schemas_response.json()
34
+ print(f"βœ… Found {len(schemas)} schemas in database:")
35
+ for schema in schemas:
36
+ print(f" - {schema['schema_id']}: {schema['title']}")
37
+ else:
38
+ print(f"❌ Failed to fetch schemas: {schemas_response.status_code}")
39
+ return False
40
+
41
+ # Test validation stats
42
+ stats_response = requests.get(f"{BASE_URL}/api/schemas/validation-stats", headers=headers)
43
+ if stats_response.status_code == 200:
44
+ stats = stats_response.json()
45
+ print(f"βœ… Validation stats: {stats['validation_passed']} passed, {stats['validation_failed']} failed")
46
+ else:
47
+ print(f"❌ Failed to fetch validation stats: {stats_response.status_code}")
48
+
49
+ return True
50
+
51
+ except Exception as e:
52
+ print(f"❌ Schema endpoint test failed: {e}")
53
+ return False
54
+
55
+ def test_schema_validation():
56
+ """Test schema validation directly"""
57
+ print("\nπŸ§ͺ Testing Schema Validation Logic...")
58
+
59
+ # Test crisis map validation
60
+ crisis_data = {
61
+ "analysis": "A major earthquake occurred in Panama with magnitude 6.6",
62
+ "metadata": {
63
+ "title": "Panama Earthquake July 2025",
64
+ "source": "WFP",
65
+ "type": "EARTHQUAKE",
66
+ "countries": ["PA"],
67
+ "epsg": "32617"
68
+ }
69
+ }
70
+
71
+ try:
72
+ from app.services.schema_validator import schema_validator
73
+
74
+ # Test crisis map validation
75
+ is_valid, error = schema_validator.validate_crisis_map_data(crisis_data)
76
+ if is_valid:
77
+ print("βœ… Crisis map validation: PASSED")
78
+ else:
79
+ print(f"❌ Crisis map validation: FAILED - {error}")
80
+
81
+ # Test drone validation
82
+ drone_data = {
83
+ "analysis": "Drone image shows damaged infrastructure after earthquake",
84
+ "metadata": {
85
+ "title": "Damaged Infrastructure",
86
+ "source": "WFP",
87
+ "type": "EARTHQUAKE",
88
+ "countries": ["PA"],
89
+ "epsg": "4326",
90
+ "center_lat": 8.5,
91
+ "center_lon": -80.0,
92
+ "amsl_m": 100.0,
93
+ "agl_m": 50.0,
94
+ "heading_deg": 180.0,
95
+ "yaw_deg": 0.0,
96
+ "pitch_deg": 0.0,
97
+ "roll_deg": 0.0,
98
+ "rtk_fix": True,
99
+ "std_h_m": 0.5,
100
+ "std_v_m": 0.3
101
+ }
102
+ }
103
+
104
+ is_valid, error = schema_validator.validate_drone_data(drone_data)
105
+ if is_valid:
106
+ print("βœ… Drone validation: PASSED")
107
+ else:
108
+ print(f"❌ Drone validation: FAILED - {error}")
109
+
110
+ # Test invalid data
111
+ invalid_data = {
112
+ "analysis": "Test analysis",
113
+ "metadata": {
114
+ "center_lat": 95.0, # Invalid: > 90
115
+ "center_lon": 200.0, # Invalid: > 180
116
+ }
117
+ }
118
+
119
+ is_valid, error = schema_validator.validate_drone_data(invalid_data)
120
+ if not is_valid:
121
+ print("βœ… Invalid data rejection: PASSED")
122
+ print(f" Expected error: {error}")
123
+ else:
124
+ print("❌ Invalid data rejection: FAILED - Should have rejected invalid coordinates")
125
+
126
+ return True
127
+
128
+ except Exception as e:
129
+ print(f"❌ Schema validation test failed: {e}")
130
+ return False
131
+
132
+ def test_models_api():
133
+ """Test models API to see debug info"""
134
+ print("\nπŸ§ͺ Testing Models API (Debug Info)...")
135
+
136
+ try:
137
+ response = requests.get(f"{BASE_URL}/api/models")
138
+ if response.status_code == 200:
139
+ data = response.json()
140
+ models = data.get("models", [])
141
+ debug = data.get("debug", {})
142
+
143
+ print(f"βœ… Found {len(models)} models:")
144
+ for model in models[:3]: # Show first 3
145
+ print(f" - {model['m_code']}: {model['label']} ({'Available' if model['is_available'] else 'Disabled'})")
146
+
147
+ if debug:
148
+ print(f"βœ… Debug info:")
149
+ print(f" - Registered services: {debug.get('registered_services', [])}")
150
+ print(f" - Total services: {debug.get('total_services', 0)}")
151
+ print(f" - Available DB models: {debug.get('available_db_models', [])}")
152
+
153
+ return True
154
+ else:
155
+ print(f"❌ Models API failed: {response.status_code}")
156
+ return False
157
+
158
+ except Exception as e:
159
+ print(f"❌ Models API test failed: {e}")
160
+ return False
161
+
162
+ def test_caption_generation():
163
+ """Test caption generation with a sample image to see if validation works"""
164
+ print("\nπŸ§ͺ Testing Caption Generation with Schema Validation...")
165
+
166
+ # This would require an actual image upload, which is complex
167
+ # For now, we'll just check if the endpoint exists
168
+ try:
169
+ # Test if caption endpoint exists (should return 404 for non-existent image)
170
+ response = requests.post(f"{BASE_URL}/api/images/test-id/caption",
171
+ data={"title": "Test", "prompt": "DEFAULT_CRISIS_MAP"})
172
+
173
+ # We expect 404 since the image doesn't exist, but that means the endpoint works
174
+ if response.status_code == 404:
175
+ print("βœ… Caption generation endpoint: ACCESSIBLE")
176
+ print(" (404 expected for non-existent image)")
177
+ return True
178
+ else:
179
+ print(f"βœ… Caption generation endpoint: ACCESSIBLE (status: {response.status_code})")
180
+ return True
181
+
182
+ except Exception as e:
183
+ print(f"❌ Caption generation test failed: {e}")
184
+ return False
185
+
186
+ def main():
187
+ print("πŸš€ Starting Schema Validation Integration Tests")
188
+ print("=" * 60)
189
+
190
+ tests = [
191
+ ("Schema Validation Logic", test_schema_validation),
192
+ ("Models API", test_models_api),
193
+ ("Caption Generation Endpoint", test_caption_generation),
194
+ ("Admin Schema Endpoints", test_schema_endpoints),
195
+ ]
196
+
197
+ results = []
198
+ for test_name, test_func in tests:
199
+ try:
200
+ result = test_func()
201
+ results.append((test_name, result))
202
+ except Exception as e:
203
+ print(f"❌ {test_name}: EXCEPTION - {e}")
204
+ results.append((test_name, False))
205
+
206
+ print("\n" + "=" * 60)
207
+ print("πŸ“Š Test Results Summary:")
208
+
209
+ passed = 0
210
+ for test_name, result in results:
211
+ status = "βœ… PASSED" if result else "❌ FAILED"
212
+ print(f" {test_name}: {status}")
213
+ if result:
214
+ passed += 1
215
+
216
+ print(f"\n🎯 Overall: {passed}/{len(results)} tests passed")
217
+
218
+ if passed == len(results):
219
+ print("\nπŸŽ‰ All tests passed! Schema validation is working correctly.")
220
+ print("\nNext steps:")
221
+ print("1. Try uploading an image through the frontend")
222
+ print("2. Check the admin panel for schema validation stats")
223
+ print("3. Look at the backend logs for validation debug messages")
224
+ else:
225
+ print("\n⚠️ Some tests failed. Check the error messages above.")
226
+ print("Common issues:")
227
+ print("- ADMIN_PASSWORD not set correctly")
228
+ print("- Backend not running on localhost:8000")
229
+ print("- Database connection issues")
230
+
231
+ if __name__ == "__main__":
232
+ main()
py_backend/test_schema_validation.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script for schema validation
4
+ """
5
+
6
+ import sys
7
+ import os
8
+ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
9
+
10
+ from app.services.schema_validator import schema_validator
11
+
12
+ def test_crisis_map_validation():
13
+ """Test crisis map data validation"""
14
+ print("Testing Crisis Map Validation...")
15
+
16
+ # Valid crisis map data
17
+ valid_data = {
18
+ "analysis": "A major earthquake occurred in Panama with magnitude 6.6",
19
+ "metadata": {
20
+ "title": "Panama Earthquake July 2025",
21
+ "source": "WFP",
22
+ "type": "EARTHQUAKE",
23
+ "countries": ["PA"],
24
+ "epsg": "32617"
25
+ }
26
+ }
27
+
28
+ is_valid, error = schema_validator.validate_crisis_map_data(valid_data)
29
+ print(f"βœ“ Valid data: {is_valid}, Error: {error}")
30
+
31
+ # Invalid crisis map data (missing required fields)
32
+ invalid_data = {
33
+ "analysis": "A major earthquake occurred in Panama",
34
+ "metadata": {
35
+ "title": "Panama Earthquake",
36
+ # Missing source, type, countries, epsg
37
+ }
38
+ }
39
+
40
+ is_valid, error = schema_validator.validate_crisis_map_data(invalid_data)
41
+ print(f"βœ— Invalid data: {is_valid}, Error: {error}")
42
+
43
+ # Test data cleaning
44
+ cleaned_data, is_valid, error = schema_validator.clean_and_validate_data(valid_data, "crisis_map")
45
+ print(f"βœ“ Cleaned data: {is_valid}, Error: {error}")
46
+ if is_valid:
47
+ print(f" Cleaned metadata: {cleaned_data['metadata']}")
48
+
49
+ def test_drone_validation():
50
+ """Test drone data validation"""
51
+ print("\nTesting Drone Validation...")
52
+
53
+ # Valid drone data
54
+ valid_data = {
55
+ "analysis": "Drone image shows damaged infrastructure after earthquake",
56
+ "metadata": {
57
+ "title": "Damaged Infrastructure",
58
+ "source": "WFP",
59
+ "type": "EARTHQUAKE",
60
+ "countries": ["PA"],
61
+ "epsg": "4326",
62
+ "center_lat": 8.5,
63
+ "center_lon": -80.0,
64
+ "amsl_m": 100.0,
65
+ "agl_m": 50.0,
66
+ "heading_deg": 180.0,
67
+ "yaw_deg": 0.0,
68
+ "pitch_deg": 0.0,
69
+ "roll_deg": 0.0,
70
+ "rtk_fix": True,
71
+ "std_h_m": 0.5,
72
+ "std_v_m": 0.3
73
+ }
74
+ }
75
+
76
+ is_valid, error = schema_validator.validate_drone_data(valid_data)
77
+ print(f"βœ“ Valid drone data: {is_valid}, Error: {error}")
78
+
79
+ # Invalid drone data (invalid coordinate values)
80
+ invalid_data = {
81
+ "analysis": "Drone image shows damaged infrastructure",
82
+ "metadata": {
83
+ "title": "Damaged Infrastructure",
84
+ "center_lat": 95.0, # Invalid: > 90
85
+ "center_lon": 200.0, # Invalid: > 180
86
+ "heading_deg": 400.0, # Invalid: > 360
87
+ }
88
+ }
89
+
90
+ is_valid, error = schema_validator.validate_drone_data(invalid_data)
91
+ print(f"βœ— Invalid drone data: {is_valid}, Error: {error}")
92
+
93
+ # Test data cleaning
94
+ cleaned_data, is_valid, error = schema_validator.clean_and_validate_data(valid_data, "drone_image")
95
+ print(f"βœ“ Cleaned drone data: {is_valid}, Error: {error}")
96
+ if is_valid:
97
+ print(f" Cleaned metadata keys: {list(cleaned_data['metadata'].keys())}")
98
+
99
+ def test_vlm_response_format():
100
+ """Test handling of different VLM response formats"""
101
+ print("\nTesting VLM Response Format Handling...")
102
+
103
+ # Test content-wrapped format
104
+ content_wrapped = {
105
+ "content": {
106
+ "analysis": "Test analysis",
107
+ "metadata": {
108
+ "title": "Test Title",
109
+ "source": "WFP",
110
+ "type": "EARTHQUAKE",
111
+ "countries": ["PA"],
112
+ "epsg": "4326"
113
+ }
114
+ }
115
+ }
116
+
117
+ cleaned_data, is_valid, error = schema_validator.clean_and_validate_data(content_wrapped, "crisis_map")
118
+ print(f"βœ“ Content-wrapped format: {is_valid}, Error: {error}")
119
+
120
+ # Test string content format
121
+ string_content = {
122
+ "content": '{"analysis": "Test", "metadata": {"title": "Test", "source": "WFP", "type": "EARTHQUAKE", "countries": ["PA"], "epsg": "4326"}}'
123
+ }
124
+
125
+ cleaned_data, is_valid, error = schema_validator.clean_and_validate_data(string_content, "crisis_map")
126
+ print(f"βœ“ String content format: {is_valid}, Error: {error}")
127
+
128
+ if __name__ == "__main__":
129
+ print("Schema Validation Test Suite")
130
+ print("=" * 40)
131
+
132
+ try:
133
+ test_crisis_map_validation()
134
+ test_drone_validation()
135
+ test_vlm_response_format()
136
+ print("\nβœ… All tests completed!")
137
+ except Exception as e:
138
+ print(f"\n❌ Test failed with error: {e}")
139
+ import traceback
140
+ traceback.print_exc()
py_backend/test_simple_validation.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Simple test to verify schema validation is working in your running backend
4
+ """
5
+
6
+ def test_validation_system():
7
+ """Test the validation system directly"""
8
+ print("πŸ§ͺ Testing Schema Validation System...")
9
+
10
+ try:
11
+ # Import the validator
12
+ from app.services.schema_validator import schema_validator
13
+
14
+ print("\n1. Testing Crisis Map Validation:")
15
+ crisis_data = {
16
+ "analysis": "A major earthquake occurred in Panama with magnitude 6.6",
17
+ "metadata": {
18
+ "title": "Panama Earthquake July 2025",
19
+ "source": "WFP",
20
+ "type": "EARTHQUAKE",
21
+ "countries": ["PA"],
22
+ "epsg": "32617"
23
+ }
24
+ }
25
+
26
+ cleaned_data, is_valid, error = schema_validator.clean_and_validate_data(crisis_data, "crisis_map")
27
+ print(f" βœ… Valid crisis map data: {is_valid}")
28
+ if is_valid:
29
+ print(f" πŸ“‹ Cleaned metadata keys: {list(cleaned_data['metadata'].keys())}")
30
+
31
+ print("\n2. Testing Drone Validation:")
32
+ drone_data = {
33
+ "analysis": "Drone shows damaged building",
34
+ "metadata": {
35
+ "title": "Damaged Building",
36
+ "center_lat": 8.5,
37
+ "center_lon": -80.0,
38
+ "amsl_m": 100.0,
39
+ "heading_deg": 180.0
40
+ }
41
+ }
42
+
43
+ cleaned_data, is_valid, error = schema_validator.clean_and_validate_data(drone_data, "drone_image")
44
+ print(f" βœ… Valid drone data: {is_valid}")
45
+ if is_valid:
46
+ print(f" πŸ“‹ Cleaned metadata has {len([k for k, v in cleaned_data['metadata'].items() if v is not None])} non-null fields")
47
+
48
+ print("\n3. Testing Invalid Data Rejection:")
49
+ invalid_data = {
50
+ "analysis": "Test",
51
+ "metadata": {
52
+ "center_lat": 95.0, # Invalid: > 90
53
+ "center_lon": 200.0, # Invalid: > 180
54
+ "heading_deg": 400.0 # Invalid: > 360
55
+ }
56
+ }
57
+
58
+ cleaned_data, is_valid, error = schema_validator.clean_and_validate_data(invalid_data, "drone_image")
59
+ print(f" βœ… Invalid data correctly rejected: {not is_valid}")
60
+ if not is_valid:
61
+ print(f" πŸ“ Error message: {error[:100]}...")
62
+
63
+ print("\n4. Testing VLM Response Format Handling:")
64
+ wrapped_data = {
65
+ "content": {
66
+ "analysis": "Test analysis",
67
+ "metadata": {
68
+ "title": "Test Title",
69
+ "source": "WFP",
70
+ "type": "EARTHQUAKE",
71
+ "countries": ["PA"],
72
+ "epsg": "4326"
73
+ }
74
+ }
75
+ }
76
+
77
+ cleaned_data, is_valid, error = schema_validator.clean_and_validate_data(wrapped_data, "crisis_map")
78
+ print(f" βœ… Content-wrapped format handled: {is_valid}")
79
+
80
+ print("\nπŸŽ‰ Schema validation system is working correctly!")
81
+ print("\nπŸ“‹ What this means:")
82
+ print(" β€’ VLM responses will be validated against schemas")
83
+ print(" β€’ Invalid data will be caught and logged")
84
+ print(" β€’ Clean, structured data will be stored")
85
+ print(" β€’ Different schemas for crisis maps vs drone images")
86
+
87
+ return True
88
+
89
+ except Exception as e:
90
+ print(f"❌ Test failed: {e}")
91
+ import traceback
92
+ traceback.print_exc()
93
+ return False
94
+
95
+ def test_database_schemas():
96
+ """Test database schema access"""
97
+ print("\nπŸ§ͺ Testing Database Schema Access...")
98
+
99
+ try:
100
+ from app import crud, database
101
+ from app.models import JSONSchema
102
+
103
+ # Create a database session
104
+ db = database.SessionLocal()
105
+
106
+ # Get schemas from database
107
+ schemas = crud.get_all_schemas(db)
108
+ print(f" βœ… Found {len(schemas)} schemas in database:")
109
+
110
+ for schema in schemas:
111
+ print(f" - {schema.schema_id}: {schema.title}")
112
+
113
+ # Test specific schema retrieval
114
+ crisis_schema = crud.get_schema(db, "[email protected]")
115
+ drone_schema = crud.get_schema(db, "[email protected]")
116
+
117
+ if crisis_schema:
118
+ print(f" βœ… Crisis schema found: {crisis_schema.title}")
119
+ if drone_schema:
120
+ print(f" βœ… Drone schema found: {drone_schema.title}")
121
+
122
+ db.close()
123
+ return True
124
+
125
+ except Exception as e:
126
+ print(f"❌ Database schema test failed: {e}")
127
+ return False
128
+
129
+ if __name__ == "__main__":
130
+ print("πŸš€ Testing Schema Validation Integration")
131
+ print("=" * 50)
132
+
133
+ success1 = test_validation_system()
134
+ success2 = test_database_schemas()
135
+
136
+ if success1 and success2:
137
+ print(f"\nβœ… All tests passed! Your schema validation is ready to use.")
138
+ print(f"\nπŸ”§ To see it in action:")
139
+ print(f" 1. Upload an image through your frontend")
140
+ print(f" 2. Check backend logs for validation messages")
141
+ print(f" 3. Use admin panel to view validation stats")
142
+ else:
143
+ print(f"\n❌ Some tests failed. Check the errors above.")