diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fb6a8ac --- /dev/null +++ b/.env.example @@ -0,0 +1,22 @@ +# Entra ID OAuth Configuration +# Get these values from your Azure App Registration + +# Azure AD Tenant ID (e.g., "contoso.onmicrosoft.com" or full UUID) +VITE_ENTRA_TENANT_ID=your-tenant-id + +# Application (Client) ID from Azure App Registration +VITE_ENTRA_CLIENT_ID=your-client-id + +# API Server URL (where the OAuth endpoints are hosted) +VITE_API_URL=http://localhost:8000/api + +# OAuth Redirect URI (must match Azure App Registration) +# For development, this will be http://localhost:5173 +# For production, use your domain e.g., https://console.company.com +VITE_ENTRA_REDIRECT_URI=http://localhost:5173/auth/callback + +# Optional: Token audience for validation (defaults to client_id) +VITE_ENTRA_AUDIENCE=api://your-client-id + +# Optional: Enable/disable Entra ID login (useful for testing) +VITE_ENABLE_ENTRA_ID=true diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..8fdd954 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 \ No newline at end of file diff --git a/CODE_SNIPPETS_TO_ADD.md b/CODE_SNIPPETS_TO_ADD.md new file mode 100644 index 0000000..3b06ecb --- /dev/null +++ b/CODE_SNIPPETS_TO_ADD.md @@ -0,0 +1,328 @@ +# Entra ID OAuth - Code Snippets to Add + +This file contains exact code snippets to add to your existing files. + +## 1. Update App.tsx + +### Add Import +**Location**: After line 26 (after existing imports) + +```typescript +import OAuthCallbackPage from "@/components/pages/oauth-callback" +``` + +### Update App Component +**Location**: Replace the login section (around line 41-57) + +**BEFORE:** +```typescript +function App() { + const { state, handlers } = useNeoApi() + + if (!state.token) { + return ( + + + { }} // Controlled by state + onComplete={() => window.location.reload()} + /> + + ) + } + // ... rest of component +} +``` + +**AFTER:** +```typescript +function App() { + const { state, handlers } = useNeoApi() + + if (!state.token) { + return ( + + + + } /> + + } /> + + + { }} // Controlled by state + onComplete={() => window.location.reload()} + /> + + ) + } + // ... rest of component +} +``` + +--- + +## 2. Add Handler to useNeoApi.ts + +### Add Handler Function +**Location**: After the `handleLogout` function (around line 1340) + +```typescript +const handleEntraIdLogin = useCallback( + async (entraAccessToken: string) => { + appLogger.info("Exchanging Entra ID OAuth token for Neo API session") + + // Clear any existing state before attempting new login + clearSystemData() + apiRef.current.clearCache() + setToken(null) + + try { + const api = apiRef.current + + // For Entra ID login, the access token from OAuth can be used directly + // as it's issued by our API's /token endpoint with PKCE validation + const newToken = entraAccessToken + + // Fetch system data using the Entra ID token + const data = await api.fetchSystemData(newToken) + + applySystemData(data) + setToken(newToken) + setCacheStats(api.getCacheStats()) + + if (data.me) { + toast.success(`Welcome, ${data.me.username}`) + } else { + toast.success("Welcome") + } + + appLogger.info("Successfully logged in with Entra ID", undefined, { + userId: data.me?.id, + username: data.me?.username, + }) + } catch (error) { + clearSystemData() + setToken(null) + + // Provide user-friendly error messages + if (error instanceof AuthenticationError) { + toast.error(error.message) + } else if (error instanceof Error) { + toast.error(`Entra ID login failed: ${error.message}`) + } else { + toast.error("Entra ID login failed. Please try again") + } + + appLogger.error( + "Entra ID login failed", + error instanceof Error ? error.message : "Unknown error" + ) + throw error + } + }, + [applySystemData, clearSystemData] +) +``` + +### Add to Handlers Return Object +**Location**: In the handlers object return (around line 1390+) + +Find this section: +```typescript + return { + state: { + // ... state properties + }, + handlers: { + handleConnect, + handleRefresh, + // ... other handlers + handleLogout, + // ADD THE NEW HANDLER HERE: + handleEntraIdLogin, + // ... rest of handlers + }, + } +} +``` + +So the full handlers object should include: +```typescript +handlers: { + handleConnect, + handleRefresh, + handleDeleteShare, + handleAddShare, + handleUpdateShare, + handleStartCrawl, + handleFetchShareDetails, + handleAddUser, + handleChangePassword, + handleFetchFileMetadata, + handleSelectFilesShare, + handleSearchFiles, + handleFetchMonitoring, + handleFetchTasks, + handleDeleteTask, + handleLogout, + handleEntraIdLogin, // ADD THIS + handleFilesPageChange, + // ... rest of handlers +} +``` + +--- + +## 3. Create .env.local File + +**Location**: Project root (same level as package.json) + +**Content**: +```env +# Entra ID OAuth Configuration +VITE_ENTRA_TENANT_ID=your-azure-ad-tenant-id-here + +VITE_ENTRA_CLIENT_ID=your-app-client-id-here + +VITE_API_URL=http://localhost:8000/api + +VITE_ENTRA_REDIRECT_URI=http://localhost:5173/auth/callback + +# Optional +VITE_ENTRA_AUDIENCE=api://your-app-client-id-here + +VITE_ENABLE_ENTRA_ID=true +``` + +Replace: +- `your-azure-ad-tenant-id-here` → Your actual Azure AD tenant ID (e.g., "contoso.onmicrosoft.com" or UUID) +- `your-app-client-id-here` → Your app registration's client ID from Azure Portal +- Port numbers → Adjust if your dev/prod servers run on different ports + +--- + +## 4. Configure Backend OAuth (One-time) + +Once you have Azure App Registration details, configure the backend: + +```bash +curl -X POST http://localhost:8000/api/v1/setup/mcp \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer " \ + -d '{ + "tenant_id": "your-tenant-id", + "client_id": "your-client-id", + "client_secret": "your-client-secret", + "audience": "api://your-client-id" + }' +``` + +Or if setup mode is enabled: +```bash +curl -X POST http://localhost:8000/api/v1/setup/mcp \ + -H "Content-Type: application/json" \ + -d '{ + "tenant_id": "your-tenant-id", + "client_id": "your-client-id", + "client_secret": "your-client-secret", + "audience": "api://your-client-id" + }' +``` + +--- + +## Testing Your Implementation + +### 1. Verify Environment Variables Are Loaded +Open DevTools console and check: +```javascript +console.log(import.meta.env.VITE_ENTRA_TENANT_ID) +console.log(import.meta.env.VITE_ENTRA_CLIENT_ID) +console.log(import.meta.env.VITE_ENTRA_REDIRECT_URI) +``` + +Should show your actual values (not "undefined"). + +### 2. Verify OAuth Service Initialized +In console: +```javascript +import { entraIdOAuthService } from './src/services/entra-id-auth.ts' +console.log(entraIdOAuthService.isConfigured()) +``` + +Should return `true` if configured correctly. + +### 3. Test OAuth Flow +1. Click "Sign in with Entra ID" button +2. You should be redirected to Entra ID login +3. After authentication, redirected to `http://localhost:5173/auth/callback?code=...&state=...` +4. Should automatically exchange code and log you in + +### 4. Check SessionStorage +Open DevTools → Application → Session Storage: +- Should see `entra_oauth_token` key with access token +- Should see `entra_oauth_state` cleared after use + +--- + +## Troubleshooting Code Additions + +### If "handleEntraIdLogin is not a function" +- Ensure you added the handler function +- Ensure you added it to the handlers return object +- Make sure the function name matches exactly (case-sensitive) +- Restart dev server + +### If "OAuthCallbackPage is not defined" +- Check import statement is correct: `import OAuthCallbackPage from "@/components/pages/oauth-callback"` +- Verify file exists at `src/components/pages/oauth-callback.tsx` +- Restart dev server + +### If "HashRouter is not defined" +- Ensure HashRouter is imported from react-router-dom at top of App.tsx +- It should already be imported, but check it's there + +### If redirect loop +- Check your redirect URI matches exactly in: + 1. `.env.local` (`VITE_ENTRA_REDIRECT_URI`) + 2. Azure App Registration + 3. Backend OAuth config +- All three must match exactly + +--- + +## Summary of Changes + +| File | Type | Location | Change | +|------|------|----------|--------| +| `src/App.tsx` | Modify | Line 26 | Add OAuthCallbackPage import | +| `src/App.tsx` | Modify | Line 41-57 | Wrap LoginPage in HashRouter with callback route | +| `src/hooks/useNeoApi.ts` | Add | After handleLogout | Add handleEntraIdLogin function | +| `src/hooks/useNeoApi.ts` | Modify | Handlers return | Add handleEntraIdLogin to object | +| `.env.local` | Create | Project root | Add OAuth configuration | +| Backend API | Configure | One-time | Call `/api/v1/setup/mcp` endpoint | + +--- + +## Files Already Created + +These files are complete and ready to use: +- ✅ `src/services/entra-id-auth.ts` +- ✅ `src/hooks/useEntraIdAuth.ts` +- ✅ `src/components/pages/oauth-callback.tsx` +- ✅ `src/components/pages/login-page.tsx` (updated) +- ✅ `.env.example` +- ✅ Documentation files + +No changes needed to these files! diff --git a/CONTRIBUTOR.md b/CONTRIBUTOR.md index 4d0cb0c..41c303f 100644 --- a/CONTRIBUTOR.md +++ b/CONTRIBUTOR.md @@ -54,7 +54,7 @@ neo-ui-framework/ ├── public/ # Static assets ├── Dockerfile # Production container image ├── docker-compose.yml # Development orchestration -├── nginx.conf # nginx configuration template +├── Caddyfile # Caddy reverse proxy configuration ├── entrypoint.sh # Container startup script └── vite.config.ts # Vite configuration ``` diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..1d69c34 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,39 @@ +:80 { + # Reverse proxy API requests to backend + # The NEO_API env var is resolved dynamically by Caddy + handle /api/* { + uri strip_prefix /api + reverse_proxy {$NEO_API} { + header_up Host {host} + header_up X-Real-IP {remote_host} + flush_interval -1 + } + } + + # Serve static UI files (catch-all, mutually exclusive with /api/*) + handle { + root * /srv + try_files {path} /index.html + file_server + } + + # Cache static assets (JS, CSS, images, fonts) + @static path *.js *.css *.png *.jpg *.jpeg *.gif *.ico *.svg *.woff *.woff2 *.ttf *.eot + header @static Cache-Control "public, immutable, max-age=31536000" + + # Prevent caching of HTML pages so auth state is always checked on refresh + @html { + not path *.js *.css *.png *.jpg *.jpeg *.gif *.ico *.svg *.woff *.woff2 *.ttf *.eot + not path /api/* + } + header @html Cache-Control "no-store, no-cache, must-revalidate" + + # Security headers + header X-Frame-Options "SAMEORIGIN" + header X-Content-Type-Options "nosniff" + header X-XSS-Protection "1; mode=block" + header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self' https://api.github.com https://netapp.github.io;" + + # Gzip compression + encode gzip +} diff --git a/Dockerfile b/Dockerfile index b0f9ae2..3f9220d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,26 +1,15 @@ -FROM nginx:1.27-alpine +FROM docker.io/library/caddy:2-alpine -# Remove default nginx config -RUN rm -f /etc/nginx/conf.d/default.conf - -# Copy template (for Docker usage) -COPY nginx.conf /etc/nginx/conf.d/default.conf.template +# Copy Caddyfile +COPY Caddyfile /etc/caddy/Caddyfile # Copy entrypoint script COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh # Copy built application -COPY dist /usr/share/nginx/html - -# Make config directory writable -RUN chown -R nginx:nginx /etc/nginx/conf.d/ && \ - chmod -R 755 /etc/nginx/conf.d/ && \ - chown -R nginx:nginx /usr/share/nginx/html +COPY dist /srv EXPOSE 80 -# Run as root to allow config file creation -USER root - ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/ENTRA_ID_LOGIN_ANALYSIS.md b/ENTRA_ID_LOGIN_ANALYSIS.md new file mode 100644 index 0000000..d12ad41 --- /dev/null +++ b/ENTRA_ID_LOGIN_ANALYSIS.md @@ -0,0 +1,472 @@ +# Entra ID UI Login via MCP OAuth - Analysis & Implementation Plan + +## Executive Summary + +**Feasibility: YES - HIGHLY DOABLE** ✅ + +The NetApp API already has comprehensive OAuth 2.0 infrastructure and Entra ID integration for MCP clients. This can be extended to support Entra ID authentication for UI users with minimal changes. + +--- + +## Current OAuth/Entra ID Capabilities in API + +### 1. **MCP OAuth Setup Endpoints** + +#### `/api/v1/setup/mcp` - Configure MCP OAuth (POST/GET) +- **Purpose**: Configure OAuth 2.0 credentials for MCP client authentication +- **Requires**: + - `tenant_id`: Azure AD Tenant ID + - `client_id`: Application (client) ID + - `client_secret`: Client secret value + - `audience`: Token audience (optional, defaults to client_id) +- **Status**: Configurable via API + +#### `/api/v1/setup/mcp/api-key` - MCP API Key (POST/GET/DELETE) +- **Purpose**: Allows API key based authentication as alternative to OAuth +- **Returns**: Generated secure key (shown only once) +- **Use Case**: For non-interactive OAuth flows + +### 2. **OAuth 2.0 Authorization Flow Endpoints** + +The API implements a **full OAuth 2.0 Authorization Code flow**: + +| Endpoint | Purpose | Current Use | +|----------|---------|-------------| +| `/.well-known/oauth-authorization-server` | OAuth metadata (RFC8414) | MCP clients | +| `/.well-known/openid-configuration` | OpenID Connect discovery | MCP clients | +| `/.well-known/jwks.json` | JWT public key set | Token validation | +| `/authorize` | OAuth authorization endpoint | Redirect to Entra ID | +| `/token` | Token exchange endpoint | Get JWT tokens | +| `/oauth/callback` | Callback from Entra ID | Handle auth code | +| `/userinfo` | OpenID UserInfo endpoint | Get user claims | +| `/register` | Dynamic client registration | RFC 7591 support | + +### 3. **Standard OAuth Endpoints** + +#### `/token` (POST) +- **Supports**: + - OAuth 2.0 Authorization Code flow (exchanges code for tokens) + - Password grant (local user authentication) + - PKCE (Proof Key for Code Exchange) for enhanced security +- **Returns**: JWT access token + token type + +#### `/authorize` (GET) +- **Parameters**: + - `response_type`: code + - `client_id`: Your app registration + - `redirect_uri`: Where to send auth code + - `scope`: openid profile email (OpenID Connect) + - `state`: CSRF protection + - `code_challenge`/`code_challenge_method`: PKCE + - `nonce`: Replay protection + - `prompt`: UI hint (login, consent, etc.) +- **Behavior**: Redirects to Microsoft Entra ID for user authentication + +#### `/oauth/callback` (GET) +- **Receives**: Authorization code from Entra ID +- **Returns**: Forwards code to client UI +- **Handles**: Error responses from Entra ID + +### 4. **Protected Resource Metadata Endpoints** (RFC 9728) + +- `/.well-known/oauth-protected-resource/mcp` +- `/.well-known/oauth-protected-resource` +- `/mcp/.well-known/oauth-protected-resource` + +These provide OAuth Protected Resource metadata needed for client authorization. + +### 5. **Current Authentication Methods** + +The API supports: +1. **OAuth2 Password Bearer** (local users via `/token` POST) +2. **JWT Bearer Token** (in Authorization header) +3. **OAuth 2.0 Code Flow** (redirect-based for MCP/UI) +4. **API Keys** (for MCP non-interactive auth) + +--- + +## Current UI Login System + +### Current Implementation +- **File**: [src/components/pages/login-page.tsx](src/components/pages/login-page.tsx) +- **Method**: Likely local username/password or basic auth +- **Token Storage**: JWT in localStorage/sessionStorage + +### What Needs Changing + +1. **Login Form**: Add "Login with Entra ID" button +2. **OAuth Flow**: Implement Authorization Code flow client-side +3. **Token Exchange**: Use `/token` endpoint to exchange code for JWT +4. **User Sync**: Link Entra ID user to local user (or create on first login) + +--- + +## Implementation Plan + +### Phase 1: Infrastructure Validation ✅ +- [x] Confirm MCP OAuth setup is available in API +- [x] Verify OAuth endpoints are implemented +- [x] Check JWT token format and claims + +### Phase 2: Frontend Implementation (2-3 days) +**Files to Create/Modify:** + +1. **Create**: `src/services/entra-id-auth.ts` + - OAuth flow orchestration + - PKCE implementation + - Token management + - Silent token refresh + +2. **Modify**: `src/components/pages/login-page.tsx` + - Add "Sign in with Entra ID" button + - Show local vs SSO options + - Handle OAuth redirect callback + +3. **Create**: `src/hooks/useEntraAuth.ts` + - OAuth state management + - Token refresh logic + - User session management + +4. **Modify**: `src/services/neo-api.tsx` + - Add Entra ID user info fetch + - Handle token refresh in API calls + - Add authorization header middleware + +5. **Create**: `src/types/auth.ts` + - OAuth token response types + - User claim types + - Entra ID user object types + +### Phase 3: Backend Configuration (1 day) +**Setup Steps:** + +1. Configure MCP OAuth via API: + ```bash + POST /api/v1/setup/mcp + { + "tenant_id": "your-tenant-id", + "client_id": "your-ui-app-client-id", + "client_secret": "your-ui-app-client-secret", + "audience": "api://your-ui-app-client-id" + } + ``` + +2. Azure App Registration Requirements: + - Redirect URIs: + - `http://localhost:5173/auth-callback` (dev) + - `https://your-domain.com/auth-callback` (prod) + - API Permissions: User.Read, OpenID, Profile, Email + - Implicit grant: Enable ID tokens for SPA + +3. Create or link Entra ID users to local users: + - Option A: Auto-create on first Entra ID login + - Option B: Require admin linking of Entra ID → local user + - Option C: Hybrid - auto-create if allowed by policy + +### Phase 4: Testing & Documentation (2-3 days) +**Test Scenarios:** + +1. Entra ID login creates user automatically +2. Entra ID user can access all shares +3. Token refresh works seamlessly +4. Logout clears session properly +5. PKCE prevents token interception +6. Multiple browser tabs stay in sync + +**Documentation:** +- Entra ID setup guide for admins +- User login instructions +- Troubleshooting guide + +--- + +## Technical Architecture + +### OAuth 2.0 Authorization Code Flow (with PKCE) + +``` +┌─────────┐ ┌────────────┐ +│ UI │ │ Entra ID │ +│(Browser)│ │ (Azure) │ +└────┬────┘ └─────┬──────┘ + │ │ + │ 1. User clicks "Sign in with Entra" │ + │ (Generate code_challenge, state, nonce) │ + │ 2. Redirect to /authorize + code_challenge │ + ├──────────────────────────────────────────────> + │ │ + │ 3. User logs in + │ │ + │ 4. Redirect back to /oauth/callback?code=..&state=.. + │<──────────────────────────────────────────────┤ + │ │ + │ 5. UI exchanges code + code_verifier │ + │ at /token endpoint │ + ├─────────────────────────────┐ │ + │ │ │ + │ 6. Calls /token │ + │ (code, code_verifier) │ + │ │ │ + │ 7. Get JWT access_token ←───┘ │ + │ │ + │ 8. Store token, call /api/v1/users/me │ + │ to get user profile │ + │ │ +``` + +### Token Storage & Refresh Strategy + +```typescript +// Token stored in sessionStorage (cleared on browser close) +{ + access_token: "eyJ0eXAiOiJKV1QiLCJhbGc...", + token_type: "bearer", + expires_in: 3600, + scope: "openid profile email", + id_token: "eyJ0eXAiOiJKV1QiLCJhbGc..." // Contains user claims +} + +// Refresh logic: +- On app load: Check token expiry +- If < 5 min to expiry: Start refresh flow +- During API calls: If 401 received, refresh token +- On logout: Clear token and redirect to /logout +``` + +--- + +## Data Model Changes + +### New Fields in `UserResponse` (Already Supported) + +```typescript +{ + id: integer, + username: string, + email: string, + is_active: boolean, + is_admin: boolean, + created_at: datetime, + last_login: datetime, + + // NEW/EXISTING Entra ID fields: + entra_object_id: string | null, // User's Entra ID object ID + entra_tenant_id: string | null, // Tenant where user exists + entra_display_name: string | null, // User's display name from Entra + entra_linked_at: datetime | null // When Entra ID was linked +} +``` + +**Good News**: The API already has these fields! (See `UserResponse` in openapi.json) + +### User Creation on First Entra Login + +```sql +INSERT INTO users ( + username, + email, + entra_object_id, + entra_tenant_id, + entra_display_name, + entra_linked_at, + is_active, + created_at +) VALUES ( + ${entraDisplayName}, + ${entraEmail}, + ${objectId}, + ${tenantId}, + ${entraDisplayName}, + NOW(), + true, + NOW() +) +``` + +--- + +## Security Considerations + +### 1. **PKCE (Proof Key for Code Exchange)** +- **Why**: Prevents authorization code interception in SPA +- **Implementation**: Already supported by endpoints +- **Required**: For public clients (SPAs) per OAuth 2.0 best practices + +### 2. **State Parameter** +- **Why**: Prevents CSRF attacks +- **Implementation**: Generate random string, validate on callback +- **Storage**: sessionStorage (scoped to single tab) + +### 3. **Nonce Parameter** +- **Why**: Prevents ID token replay attacks +- **Implementation**: Included in /authorize request, validated in JWT +- **Format**: Random string, included in JWT claims + +### 4. **Token Storage** +- **Current**: sessionStorage (cleared on browser close) +- **Recommendation**: Keep in sessionStorage (more secure than localStorage) +- **Refresh**: Use refresh token if API supports it (check /token response) + +### 5. **Token Validation** +- **Where**: On UI load and before API calls +- **What to check**: + - Signature via `/well-known/jwks.json` + - Expiry time (exp claim) + - Issuer matches Entra ID + - Audience matches your app + +### 6. **ACL Implications** +- **Current**: Files accessed via `/api/v1/shares/{share_id}/files` +- **With Entra ID**: Need to map Entra ID object ID to file ACLs +- **Backend**: May need enhancement if currently using local usernames + +--- + +## Alternative Approaches Considered + +### Option A: Use OAuth via UI (RECOMMENDED) +- ✅ User-friendly +- ✅ Uses existing OAuth infrastructure +- ✅ Leverages PKCE for security +- ✅ Integrates with Entra ID seamlessly +- ⚠️ Requires frontend OAuth library + +### Option B: Backend-Mediated OAuth +- ✅ Simpler frontend +- ⚠️ Backend becomes security bottleneck +- ⚠️ Session-based auth (less RESTful) +- ⚠️ More complex token refresh + +### Option C: SAML 2.0 Integration +- ✅ Enterprise-standard +- ⚠️ Not currently implemented +- ⚠️ Requires new backend support +- ⚠️ More complex setup + +**Recommendation**: Go with **Option A** - it's already partially implemented and most secure. + +--- + +## Required Azure App Registration Setup + +### Prerequisites +- Azure AD admin access +- Tenant ID known +- Domain/URL where UI will be hosted + +### Steps + +1. **Create App Registration** in Azure Portal + - Name: "NetApp NEO UI" + - Supported account types: "Accounts in this organizational directory only" + - Redirect URI: "Web" → `https://your-domain.com/auth-callback` + +2. **Configure API Permissions** + - Add: "Microsoft Graph" → "Delegated" + - Permissions: `User.Read`, `openid`, `profile`, `email` + +3. **Generate Client Secret** + - Create new secret (copy immediately!) + - Store securely (e.g., env var) + +4. **Configure Token Settings** + - ID tokens: Check "enable" + - Access tokens: Check "enable" + +5. **CORS / API Exposure** (if using direct OAuth flow) + - Add scope: `api:///access_as_user` + +### Environment Variables + +```bash +VITE_ENTRA_TENANT_ID=your-tenant-id +VITE_ENTRA_CLIENT_ID=your-ui-app-client-id +VITE_ENTRA_API_URL=https://your-api-domain/api +VITE_ENTRA_REDIRECT_URI=https://your-domain.com/auth-callback +``` + +--- + +## Implementation Roadmap + +### Week 1: Setup & Planning +- [ ] Get Azure AD tenant/app registration details +- [ ] Verify API OAuth endpoints working +- [ ] Set up environment variables +- [ ] Create implementation branch + +### Week 2: Frontend Development +- [ ] Build OAuth client library (`entra-id-auth.ts`) +- [ ] Create login UI with Entra button +- [ ] Implement authorization code flow +- [ ] Add token refresh logic +- [ ] Build auth callback handler + +### Week 3: Integration & Testing +- [ ] Integrate with existing user service +- [ ] Test end-to-end OAuth flow +- [ ] Test token refresh +- [ ] Test error scenarios +- [ ] Performance testing + +### Week 4: Documentation & Deployment +- [ ] Write admin setup guide +- [ ] Write user documentation +- [ ] Security audit +- [ ] Staging deployment +- [ ] Production deployment + +--- + +## Risk Assessment + +| Risk | Likelihood | Impact | Mitigation | +|------|-----------|--------|-----------| +| Token expiry during long operations | Medium | Low | Implement token refresh middleware | +| PKCE not enforced by API | Low | Medium | Verify endpoint accepts code_challenge | +| User not found after Entra login | Medium | Medium | Implement auto-user-creation or linking | +| Multiple Entra IDs per local user | Low | Medium | Enforce 1:1 mapping, add UI controls | +| ACL resolution with Entra IDs | Medium | High | Verify API resolves Entra object IDs in ACLs | +| Cross-domain CORS issues | Medium | Low | Configure CORS in API | + +--- + +## Success Criteria + +- [ ] Users can log in with Entra ID from login page +- [ ] Session persists across page reloads +- [ ] Token automatically refreshes when expired +- [ ] User profile displays Entra ID information +- [ ] File access respects Entra ID groups +- [ ] Logout clears all session data +- [ ] Works in all supported browsers +- [ ] No console errors related to auth +- [ ] < 500ms additional login time vs local auth + +--- + +## Files to Review Before Starting + +1. [src/components/pages/login-page.tsx](src/components/pages/login-page.tsx) - Current login UI +2. [src/services/neo-api.tsx](src/services/neo-api.tsx) - Current API service +3. [src/context/settings-context.tsx](src/context/settings-context.tsx) - Auth context structure +4. [src/hooks/useNeoApi.ts](src/hooks/useNeoApi.ts) - API hook patterns + +--- + +## Conclusion + +**The implementation is highly feasible.** The backend already has: +- ✅ Complete OAuth 2.0 Authorization Code flow +- ✅ Entra ID integration configured +- ✅ PKCE support +- ✅ User Entra ID field mapping +- ✅ OpenID Connect endpoints + +**What's needed on frontend:** +- OAuth flow orchestration library +- Login UI with Entra ID button +- Token refresh middleware +- Callback handler + +**Estimated effort: 1-2 weeks of development + testing** diff --git a/ENTRA_ID_OAUTH_QUICK_REFERENCE.md b/ENTRA_ID_OAUTH_QUICK_REFERENCE.md new file mode 100644 index 0000000..8a4419b --- /dev/null +++ b/ENTRA_ID_OAUTH_QUICK_REFERENCE.md @@ -0,0 +1,259 @@ +# Entra ID OAuth Implementation - Quick Summary + +## ✅ Completed Components + +### 1. OAuth Service Layer +- **File**: `src/services/entra-id-auth.ts` +- **What it does**: + - PKCE implementation for secure authorization code exchange + - OAuth 2.0 Authorization Code flow orchestration + - State management for CSRF protection + - Token storage/retrieval in sessionStorage + - Session-based token lifecycle +- **Key methods**: + - `initiateAuthorizationFlow()` - Starts OAuth flow + - `handleCallback()` - Processes authorization code + - `getStoredToken()` - Retrieves cached token + - `clearToken()` - Logout cleanup + +### 2. React Hook for OAuth +- **File**: `src/hooks/useEntraIdAuth.ts` +- **What it does**: + - Provides OAuth state to React components + - Handles token persistence across renders + - Exposes OAuth flow controls + - Manages loading and error states +- **Hook interface**: + ```typescript + { + token: OAuthTokenResponse | null + isLoading: boolean + error: string | null + isConfigured: boolean + initiateLogin: () => Promise + logout: () => void + getAccessToken: () => string | null + getTokenClaims: () => Record | null + } + ``` + +### 3. OAuth Callback Handler +- **File**: `src/components/pages/oauth-callback.tsx` +- **What it does**: + - Handles redirect from `/oauth/callback` + - Extracts `code` and `state` from URL params + - Exchanges auth code for token + - Shows loading/error UI + - Redirects back to login after token receipt + +### 4. Enhanced Login Page +- **File**: `src/components/pages/login-page.tsx` +- **Updates**: + - Added "Sign in with Entra ID" button + - Integrated OAuth hook + - Token exchange handling + - Unified error display + - Conditional rendering based on OAuth config + +### 5. Environment Configuration +- **File**: `.env.example` +- **Contains**: + - `VITE_ENTRA_TENANT_ID` - Azure AD tenant + - `VITE_ENTRA_CLIENT_ID` - App registration client ID + - `VITE_API_URL` - Backend API URL + - `VITE_ENTRA_REDIRECT_URI` - OAuth callback URL + - `VITE_ENTRA_AUDIENCE` - Optional token audience + +### 6. Documentation +- **File**: `ENTRA_ID_OAUTH_SETUP.md` - Complete setup guide +- **File**: `ENTRA_ID_LOGIN_ANALYSIS.md` - Original analysis doc + +--- + +## ⚙️ Manual Configuration Required + +### 1. Update App.tsx +**Location**: `src/App.tsx` +**Changes needed**: +- Add import: `import OAuthCallbackPage from "@/components/pages/oauth-callback"` +- Wrap login page in `` with routes: + - Route `/auth/callback` → `OAuthCallbackPage` + - Route `*` → `LoginPage` (with `onEntraIdLogin` handler) + +### 2. Create .env.local +**Location**: Project root +**Content**: Copy from `.env.example` and fill in: +- Azure AD tenant ID +- Client ID from Azure App Registration +- API URL (usually `http://localhost:8000/api` for dev) +- Redirect URI (usually `http://localhost:5173/auth/callback` for dev) + +### 3. Add Handler to useNeoApi +**Location**: `src/hooks/useNeoApi.ts` +**Add**: +- `handleEntraIdLogin` callback that exchanges Entra ID token for API session +- Should call `api.fetchSystemData(token)` and set app state like `handleConnect` does +- Add to handlers return object + +### 4. Azure App Registration +**In Azure Portal**: +1. Create app registration (or update existing one) +2. Add redirect URIs: + - Dev: `http://localhost:5173/auth/callback` + - Prod: `https://your-domain.com/auth/callback` +3. Grant API permissions (User.Read, openid, profile, email) +4. Create client secret +5. Copy: Tenant ID, Client ID, Client Secret + +### 5. Backend OAuth Configuration +**Run API call**: +```bash +curl -X POST http://localhost:8000/api/v1/setup/mcp \ + -H "Content-Type: application/json" \ + -d '{ + "tenant_id": "", + "client_id": "", + "client_secret": "", + "audience": "api://" + }' +``` + +--- + +## 🔄 How the OAuth Flow Works + +``` +User Click "Sign in with Entra ID" + ↓ +initiateEntraLogin() + ↓ +Generate PKCE challenge + state + nonce + ↓ +Save to sessionStorage + ↓ +Redirect to /authorize endpoint + ↓ +User authenticates with Entra ID + ↓ +Redirected to /oauth/callback?code=...&state=... + ↓ +handleCallback() extracts code & state + ↓ +Validates state (CSRF protection) + ↓ +POST /token with code + code_verifier (PKCE) + ↓ +Receive access_token + ↓ +Store in sessionStorage + ↓ +useEntraIdAuth hook detects token + ↓ +LoginPage exchanges for API session via onEntraIdLogin + ↓ +User logged into Neo UI +``` + +--- + +## 📦 File Structure + +``` +src/ +├── services/ +│ └── entra-id-auth.ts [NEW] OAuth 2.0 service +├── hooks/ +│ └── useEntraIdAuth.ts [NEW] OAuth React hook +├── components/ +│ └── pages/ +│ ├── oauth-callback.tsx [NEW] Callback handler +│ └── login-page.tsx [UPDATED] Added Entra ID button +└── App.tsx [TODO] Add OAuth callback route +``` + +--- + +## 🧪 Testing Checklist + +- [ ] Environment variables set in `.env.local` +- [ ] App.tsx updated with OAuth callback route +- [ ] useNeoApi.ts has `handleEntraIdLogin` handler +- [ ] Azure App Registration created with correct redirect URIs +- [ ] Backend MCP OAuth configured +- [ ] Local login still works +- [ ] Entra ID button appears on login page +- [ ] Clicking Entra ID button redirects to Microsoft login +- [ ] After authentication, redirected to callback handler +- [ ] Token exchanged successfully +- [ ] User logged into app +- [ ] SessionStorage contains `entra_oauth_token` +- [ ] Error handling works (shows error messages) +- [ ] Can log out and log back in +- [ ] Token cleared on logout + +--- + +## 🔐 Security Features Implemented + +✅ PKCE (Proof Key for Code Exchange) +✅ State parameter validation +✅ Nonce for ID token replay protection +✅ SessionStorage (cleared on browser close) +✅ Secure random string generation +✅ HTTPS ready (configure for production) +✅ Error-safe state cleanup +✅ Timeout protection (10 min state expiry) + +--- + +## 📝 Next Action Items + +1. **Today/This Week**: + - [ ] Create Azure App Registration + - [ ] Get Tenant ID, Client ID, Client Secret + - [ ] Create `.env.local` file + +2. **Day 2**: + - [ ] Update `src/App.tsx` with callback route + - [ ] Add `handleEntraIdLogin` to `useNeoApi.ts` + - [ ] Run `npm run dev` and test + +3. **Day 3+**: + - [ ] Configure backend MCP OAuth via API + - [ ] Full end-to-end testing + - [ ] Deploy to staging + - [ ] Production deployment + +--- + +## 💬 Support References + +- **Setup Guide**: See `ENTRA_ID_OAUTH_SETUP.md` +- **Analysis**: See `ENTRA_ID_LOGIN_ANALYSIS.md` +- **OAuth Service**: Check `src/services/entra-id-auth.ts` for PKCE details +- **Hook**: Check `src/hooks/useEntraIdAuth.ts` for state management +- **Callback**: Check `src/components/pages/oauth-callback.tsx` for error handling + +--- + +## 🎯 Key Design Decisions + +1. **SessionStorage** (not localStorage): + - Tokens cleared on browser close for better security + - PKCE state is one-time use + - 10-minute timeout on state validity + +2. **Lazy state decode**: + - Don't verify JWT signature (done by API) + - Only decode claims for UI purposes + - Assume tokens from our API are trustworthy + +3. **Separate local/OAuth flows**: + - Users can choose login method + - No forced account linking (can be added later) + - Clear separation of concerns + +4. **Error resilience**: + - Graceful degradation if OAuth config missing + - Clear error messages for debugging + - Fallback to local login always available diff --git a/ENTRA_ID_OAUTH_SETUP.md b/ENTRA_ID_OAUTH_SETUP.md new file mode 100644 index 0000000..7558a84 --- /dev/null +++ b/ENTRA_ID_OAUTH_SETUP.md @@ -0,0 +1,387 @@ +# Entra ID OAuth Frontend Implementation - Setup Guide + +## Overview + +This guide covers the frontend OAuth 2.0 implementation for Entra ID login in the Neo UI. The implementation is nearly complete - just a few manual configuration steps are needed. + +## What's Been Created + +### 1. **OAuth Service** (`src/services/entra-id-auth.ts`) +- Handles PKCE (Proof Key for Code Exchange) +- Manages OAuth flow (authorization code exchange) +- Secure token storage in sessionStorage +- State validation for CSRF protection + +### 2. **OAuth Hook** (`src/hooks/useEntraIdAuth.ts`) +- React hook for OAuth state management +- Token storage and retrieval +- Auth flow initiation + +### 3. **OAuth Callback Page** (`src/components/pages/oauth-callback.tsx`) +- Handles redirect from Entra ID +- Exchanges authorization code for access token +- Error handling and user feedback + +### 4. **Updated Login Page** (`src/components/pages/login-page.tsx`) +- Added "Sign in with Entra ID" button +- Entra ID token exchange handling +- Error display for both local and OAuth auth + +### 5. **Environment Configuration** (`.env.example`) +- Template for required environment variables + +--- + +## Manual Configuration Steps + +### Step 1: Update App.tsx + +Add the OAuth callback route handling. Find the App component and update it as follows: + +**File**: `src/App.tsx` + +**Update the imports:** +```typescript +import LoginPage from "@/components/pages/login-page" +import OAuthCallbackPage from "@/components/pages/oauth-callback" // Add this line +import { useNeoApi } from "@/hooks/useNeoApi" +import { SetupWizardDialog } from "@/components/dialogs/setup-wizard-dialog" +``` + +**Update the App component's login page rendering** (currently around line 43-56): + +Replace: +```typescript +if (!state.token) { + return ( + + + { }} + onComplete={() => window.location.reload()} + /> + + ) +} +``` + +With: +```typescript +if (!state.token) { + return ( + + + + } /> + + } /> + + + { }} + onComplete={() => window.location.reload()} + /> + + ) +} +``` + +### Step 2: Create Environment Variables File + +Create `.env.local` in your project root with Entra ID OAuth configuration: + +```bash +# Copy from .env.example and fill in your values +cp .env.example .env.local +``` + +**Edit `.env.local`:** +```env +# Azure AD Tenant ID (get from Azure Portal) +VITE_ENTRA_TENANT_ID=your-tenant-id + +# Application (Client) ID from Azure App Registration +VITE_ENTRA_CLIENT_ID=your-client-id + +# API Server URL +VITE_API_URL=http://localhost:8000/api + +# OAuth Redirect URI (must match Azure App Registration) +# For development: http://localhost:5173/auth/callback +# For production: https://your-domain.com/auth/callback +VITE_ENTRA_REDIRECT_URI=http://localhost:5173/auth/callback + +# Optional: Token audience +VITE_ENTRA_AUDIENCE=api://your-client-id + +# Enable/disable Entra ID (default: true) +VITE_ENABLE_ENTRA_ID=true +``` + +### Step 3: Update Handlers in useNeoApi + +The hook needs a handler for Entra ID token exchange. Find the `useNeoApi.ts` file and add: + +**File**: `src/hooks/useNeoApi.ts` + +In the `handlers` object (around line 1470-1480), add: + +```typescript +handleEntraIdLogin: useCallback(async (entraToken: string) => { + appLogger.info("Exchanging Entra ID token for Neo API token") + + try { + // The Entra ID token can be used directly as a Bearer token + // or exchanged at the /token endpoint if you have a bridge flow + // For now, use it directly + const token = entraToken + + // Get user information using the Entra ID token + const data = await api.fetchSystemData(token) + + applySystemData(data) + setToken(token) + setCacheStats(api.getCacheStats()) + + if (data.me) { + toast.success(`Welcome, ${data.me.username}`) + } else { + toast.success("Welcome") + } + + appLogger.info("Successfully logged in with Entra ID", undefined, { + userId: data.me?.id, + username: data.me?.username, + }) + } catch (error) { + clearSystemData() + setToken(null) + + if (error instanceof AuthenticationError) { + toast.error(error.message) + } else if (error instanceof Error) { + toast.error(`Entra ID login failed: ${error.message}`) + } else { + toast.error("Entra ID login failed. Please try again") + } + + appLogger.error( + "Entra ID login failed", + error instanceof Error ? error.message : "Unknown error" + ) + throw error + } +}, [applySystemData, clearSystemData]) +``` + +And add it to the handlers return object: +```typescript +handlers: { + // ... existing handlers ... + handleEntraIdLogin, + // ... rest of handlers ... +} +``` + +### Step 4: Azure App Registration Setup + +Follow the guide in `ENTRA_ID_LOGIN_ANALYSIS.md` under "Required Azure App Registration Setup" to: + +1. Create/update your app registration in Azure Portal +2. Configure redirect URIs +3. Add API permissions +4. Generate a client secret +5. Get your tenant ID and client ID + +### Step 5: Configure Backend OAuth + +Once the Azure app is configured, set up MCP OAuth on the backend: + +```bash +# Make an API request to configure MCP OAuth +curl -X POST http://localhost:8000/api/v1/setup/mcp \ + -H "Content-Type: application/json" \ + -d '{ + "tenant_id": "your-tenant-id", + "client_id": "your-client-id", + "client_secret": "your-client-secret", + "audience": "api://your-client-id" + }' +``` + +--- + +## Testing the Implementation + +### Development Testing + +1. **Start the dev server:** + ```bash + npm run dev + ``` + +2. **Test local login first:** + - Verify local username/password login works + - Make sure you can access the app normally + +3. **Test Entra ID login:** + - Click "Sign in with Entra ID" button + - You should be redirected to Entra ID login + - After logging in, you'll be redirected to `/auth/callback` + - The page should exchange the code for a token + - You should be logged into the app + +4. **Test token exchange:** + - Open browser DevTools (F12) → Application → Session Storage + - Look for `entra_oauth_token` and `entra_oauth_state` keys + - Verify the token structure + +5. **Test error handling:** + - Try canceling the Entra ID login + - Try with invalid credentials + - Verify error messages appear correctly + +### Production Testing + +1. Update `VITE_ENTRA_REDIRECT_URI` to your production domain +2. Add the production domain to Azure App Registration redirect URIs +3. Test the full OAuth flow in a staging environment +4. Verify tokens persist across page reloads (sessionStorage) + +--- + +## Troubleshooting + +### "OAuth is not configured" Error + +**Cause**: Environment variables not set +**Fix**: +- Ensure `.env.local` exists with all required variables +- Restart dev server after updating env vars +- Check that variable names are correct (must start with `VITE_`) + +### Redirect URI mismatch error from Entra ID + +**Cause**: Redirect URI in app doesn't match Azure configuration +**Fix**: +- Verify `VITE_ENTRA_REDIRECT_URI` matches Azure App Registration +- For development: use `http://localhost:5173/auth/callback` +- For production: use your actual domain + +### Token not being stored + +**Cause**: Browser privacy mode or sessionStorage disabled +**Fix**: +- Test in normal browsing mode (not private/incognito) +- Check browser console for sessionStorage errors +- Ensure cookies/storage are not blocked + +### "State parameter mismatch" error + +**Cause**: State validation failed (CSRF attack prevention) +**Fix**: +- This is rare, usually means: + - Manual URLs being constructed + - Session was cleared between redirect steps + - Clock skew between client/server +- Try clearing sessionStorage and logging in again + +### User not found after Entra ID login + +**Cause**: Entra ID user doesn't exist in Neo system +**Fix**: +- Admin needs to create user in Neo with matching Entra ID object ID +- Or implement auto-user-creation (requires backend changes) +- Check if user email matches + +--- + +## Security Considerations + +### ✅ What's Already Secured + +1. **PKCE**: Authorization code interception prevention +2. **State Parameter**: CSRF attack prevention +3. **Nonce**: ID token replay attack prevention +4. **SessionStorage**: Tokens cleared on browser close +5. **HTTPS Required**: In production, ensure all URLs are HTTPS + +### ⚠️ Additional Recommendations + +1. **Implement token refresh**: If tokens expire, implement refresh flow +2. **Validate token signature**: Optional - already done by API +3. **Add token revocation**: Call logout on server when user logs out +4. **Monitor for token leaks**: Check logs for suspicious token usage + +--- + +## API Integration Points + +### Endpoints Used + +1. **`GET /authorize`** - Redirect to Entra ID +2. **`POST /token`** - Exchange code for token +3. **`GET /oauth/callback`** - Receive callback from Entra ID +4. **`GET /api/v1/users/me`** - Get current user info +5. **`POST /logout`** - Invalidate token on logout + +### Token Format + +The token returned is a JWT with claims including: +- `sub`: Subject (user ID) +- `email`: User email +- `name`: User display name +- `oid`: Entra ID object ID +- `tid`: Tenant ID + +--- + +## Next Steps + +1. ✅ Complete Azure App Registration setup +2. ✅ Update `src/App.tsx` with OAuth callback route +3. ✅ Create `.env.local` with configuration +4. ✅ Add handler to `useNeoApi.ts` +5. ✅ Test local login first +6. ✅ Test Entra ID login flow +7. ✅ Deploy to staging +8. ✅ Test in production environment + +--- + +## FAQ + +**Q: Can I use this for both local and Entra ID auth?** +A: Yes! Users can choose between local login or Entra ID at the login screen. + +**Q: What happens if both local and Entra ID accounts exist?** +A: Currently they're separate. Consider implementing account linking for better UX. + +**Q: How long are tokens valid?** +A: Set in Azure AD (default 1 hour). Implement refresh token flow for longer sessions. + +**Q: Can I restrict login to certain Entra ID groups?** +A: Yes, add group claims validation in the token exchange handler. + +**Q: What if Entra ID OAuth fails?** +A: User can fall back to local username/password login. + +--- + +## Support + +For issues related to: +- **OAuth flow**: Check browser console for errors, review `entra-id-auth.ts` +- **Token exchange**: Check `useNeoApi.ts` handler implementation +- **Azure configuration**: See Azure App Registration setup in ENTRA_ID_LOGIN_ANALYSIS.md +- **Backend OAuth**: See API documentation for `/api/v1/setup/mcp` diff --git a/QUICKSTART.md b/QUICKSTART.md index 82545aa..c3511e6 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -22,7 +22,7 @@ For containerized deployments, configure the Neo API endpoint: docker run -e NEO_API=http://neo-backend:8080 neo-ui-framework ``` -The `entrypoint.sh` script injects this value into nginx configuration at runtime. +The `entrypoint.sh` script exports this value for Caddy to resolve at runtime. ## From source diff --git a/README.md b/README.md index 6138fb8..bc74500 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Neo UI Framework delivers a full-featured interface for managing NetApp Neo - th - **Complete API Coverage** - Full integration with Neo API including authentication, health monitoring, license management, user administration, share management, file operations, and activity logging - **Modern Tech Stack** - Built with React 19, TypeScript 5.9, Vite 7, and Tailwind CSS 4 -- **Production Ready** - Containerized deployment with nginx, Docker Compose support, and environment-based configuration +- **Production Ready** - Containerized deployment with Caddy, Docker Compose support, and environment-based configuration - **White-Label Friendly** - Easily customize branding, themes, and navigation to match your organization - **Type-Safe** - Comprehensive TypeScript coverage with strict compiler options - **Responsive Design** - Mobile-first approach with adaptive layouts and sidebar navigation diff --git a/SECURITY.md b/SECURITY.md index b0ae5a4..50ac267 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -31,3 +31,5 @@ If we verify a reported security vulnerability, our policy is: - A security advisory will be released on the project GitHub repository detailing the vulnerability, as well as recommendations for end-users to protect themselves. + +- We will work with the reporter to ensure they are credited in the security advisory. diff --git a/SUPPORT.md b/SUPPORT.md index 1c5bd49..667b2f8 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -8,8 +8,9 @@ The Neo UI framework supports the Neo backend API versions: | Version | Supported | | ------- | ------------------ | -| main | ✅ Yes | -| 2.x.x | ✅ Yes | +| 4.x.x | ✅ Yes | +| 3.x.x | ❌ No | +| 2.x.x | ❌ No | | < 2.0 | ❌ No | ## Reporting Bugs @@ -20,7 +21,7 @@ When reporting a bug, please include: * Use the provided template * A clear description of the issue. * Steps to reproduce the bug. -* A description of your environments. +* A description of your environment. * Any relevant error messages. ## Asking Questions diff --git a/caching-specs.md b/caching-specs.md new file mode 100644 index 0000000..477cf11 --- /dev/null +++ b/caching-specs.md @@ -0,0 +1,48 @@ +# Caching Mechanism Analysis + +## Executive Summary +The `neo-ui-framework` implements a client-side caching mechanism using an in-memory `DataLoader` service. This service caches API responses based on unique keys derived from the request parameters (token, share ID, page number, etc.). The caching strategy for the data corpus (Files) is primarily **on-demand (lazy loading)** and **page-based**, rather than pre-fetching the entire dataset. + +## Detailed Analysis + +### 1. Does the interface pre-cache the full pagination list for the data corpus at loading time? +**Answer: No.** + +* **Mechanism**: The application does **not** fetch the list of all files at application startup or when the session is restored. +* **Evidence**: + * The `fetchSystemData` function in `NeoApiService` (used during initialization) explicitly returns `files: null` and does not invoke the file search/list endpoints. + * File data is only requested when `handleSelectFilesShare` is triggered, typically via user interaction (selecting a share or the "All shares" view). + * Even when triggered, the request is specific to a page (defaulting to page 1) and a page size (default 100). There is no loop or background process to iterate through all pages and cache them. + +### 2. Does the interface cache the full pagination list when navigating with or without content? +**Answer: No, it strictly caches individual pages.** + +* **Mechanism**: The interface caches the *results of specific pagination requests*. It does not cache the "full" list of all files unless the user manually navigates to every single page. +* **Behavior**: + * When a user visits Page 1, the response for Page 1 is cached. + * When the user navigates to Page 2, a new request is made and cached. + * If the user navigates back to Page 1, the cached response is used (provided it hasn't expired or been evicted). + * The `DataLoader` key for file lists includes the page number `page` and `pageSize`, ensuring unique cache entries for each slice of data (e.g., `files:token:shareId:1:100`). + +### 3. Does the caching for the pagination list include the content? +**Answer: No.** + +* **Mechanism**: The pagination list endpoints (`/files` or `/search/files`) return an array of `FileEntry` objects. +* **Data Structure**: + * The `FileEntry` interface contains metadata such as filename, path, size, file type, and timestamps. + * It does **not** contain the actual file content (text or bytes). +* **Content Retrieval**: File content is only fetched when specifically requested via `getFileMetadata` (which returns `FileMetadataResponse`), and this is a separate cache entry (`fileMetadata:token:shareId:fileId`). + +## Technical Details + +### DataLoader Service +* **Implementation**: `src/services/data-loader.ts` +* **Strategy**: In-memory LRU (Least Recently Used) cache. +* **TTL (Time To Live)**: Defaults to 30 seconds for general data, but `filesTtl` (configurable, default 10 minutes) is used for file-related requests. +* **Eviction**: Based on entry size and a maximum cache size limit (default 100MB). + +### API Models +* **File List**: Returns `FilesResponse { files: FileEntry[], ... }`. + * `FileEntry` fields: `id`, `file_path`, `filename`, `size`, `created_at`, `modified_time`, `accessed_at`, `is_directory`, `file_type`. +* **File Content**: Returns `FileMetadataResponse`. + * Fields: `content`, `content_chunks`, plus metadata. diff --git a/docker-compose.yml b/docker-compose.yml index 0351f89..dd2ba52 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,7 @@ services: restart: unless-stopped netapp-neo: - image: ghcr.io/netapp/netapp-copilot-connector:2.2.6 + image: ghcr.io/netapp/netapp-copilot-connector:3.2.4 user: "1000:1000" cap_add: - SYS_ADMIN @@ -33,23 +33,13 @@ services: env_file: - .env environment: - - UID=8000 - - GID=8000 - PORT=8080 - - PYTHONUNBUFFERED=1 - - DB_PATH=data/database.db - - MS_GRAPH_CLIENT_ID=${MS_GRAPH_CLIENT_ID} - - MS_GRAPH_CLIENT_SECRET=${MS_GRAPH_CLIENT_SECRET} - - MS_GRAPH_TENANT_ID=${MS_GRAPH_TENANT_ID} - - MS_GRAPH_CONNECTOR_ID=${MS_GRAPH_CONNECTOR_ID:-netappcopilot} - - MS_GRAPH_CONNECTOR_NAME=${MS_GRAPH_CONNECTOR_NAME:-"NetApp Connector"} - - NETAPP_CONNECTOR_LICENSE=${NETAPP_CONNECTOR_LICENSE} volumes: - neo-226:/app/data restart: unless-stopped netapp-neo-ui: - image: ghcr.io/netapp/neo-ui-framework:2.2.6 + image: ghcr.io/netapp/neo-ui-framework:3.2.2 ports: - "8080:80" environment: diff --git a/entrypoint.sh b/entrypoint.sh index 962833f..8dea2d7 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -4,46 +4,14 @@ set -e NEO_API=${NEO_API:-http://localhost:8080} export NEO_API -echo "Configuring nginx to proxy API requests to: $NEO_API" +echo "Configuring Caddy to proxy API requests to: $NEO_API" -# Check if we're running in Kubernetes with a pre-configured ConfigMap -# If /etc/nginx/conf.d/default.conf exists and is not a template, use it as-is -if [ -f /etc/nginx/conf.d/default.conf ] && ! grep -q '\${NEO_API}' /etc/nginx/conf.d/default.conf 2>/dev/null; then - echo "Found pre-configured nginx config (likely from ConfigMap), skipping template substitution" - echo "Current proxy_pass configuration:" - grep proxy_pass /etc/nginx/conf.d/default.conf || echo "No proxy_pass found" -else - echo "Generating nginx config from template" - - # Check if template exists - if [ ! -f /etc/nginx/conf.d/default.conf.template ]; then - echo "Error: Template file not found at /etc/nginx/conf.d/default.conf.template" - exit 1 - fi - - # Create temp file in writable directory first - envsubst '$NEO_API' < /etc/nginx/conf.d/default.conf.template > /tmp/default.conf - - # Copy to final location with error handling - if cp -f /tmp/default.conf /etc/nginx/conf.d/default.conf 2>/dev/null; then - echo "Configuration updated successfully" - else - echo "Warning: Could not copy config file, trying direct write" - envsubst '$NEO_API' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf || { - echo "Error: Could not create nginx configuration" - exit 1 - } - fi - - echo "Checking NEO_API configuration in NGINX default.conf" - grep proxy_pass /etc/nginx/conf.d/default.conf +# Check if a pre-configured Caddyfile exists (e.g., from Kubernetes ConfigMap) +# If it does not contain the env var placeholder, use it as-is +if [ -f /etc/caddy/Caddyfile ] && ! grep -q '{\$NEO_API}' /etc/caddy/Caddyfile 2>/dev/null; then + echo "Found pre-configured Caddyfile (likely from ConfigMap), using as-is" fi -# Validate nginx configuration -if nginx -t; then - echo "Nginx configuration is valid, starting server..." - exec nginx -g 'daemon off;' -else - echo "Error: Nginx configuration test failed" - exit 1 -fi \ No newline at end of file +# Validate and run Caddy +echo "Starting Caddy server..." +exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js index b19330b..99fd689 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -19,5 +19,25 @@ export default defineConfig([ ecmaVersion: 2020, globals: globals.browser, }, - }, + rules: { + "react-refresh/only-export-components": [ + "error", + { + allowConstantExport: true, + allowExportNames: [ + "getStatusIcon", + "getStatusBadge", + "formatDuration", + "useTheme", + "useSettings", + "useSidebar", + "useFormField", + "badgeVariants", + "buttonVariants", + "toggleVariants", + ], + }, + ], + }, + }, ]) diff --git a/neocoreapi/ner.md b/neocoreapi/ner.md new file mode 100644 index 0000000..0184c01 --- /dev/null +++ b/neocoreapi/ner.md @@ -0,0 +1,412 @@ +# Named Entity Recognition (NER) + +Project Neo uses [GLiNER2](https://github.com/urchade/GLiNER) (Generalist and Lightweight model for Named Entity Recognition) to identify and extract structured entities from document text. NER runs as a dedicated microservice (`ner_service`) and processes files asynchronously via the worker queue after text extraction completes. + +## How It Works + +1. A file is uploaded to a share that has `enable_ner_analysis` turned on. +2. The worker service extracts text from the file (PDF, DOCX, etc.). +3. The worker sends the extracted text to the NER service over an internal HTTP call. +4. The NER service runs the GLiNER2 model against the text using the share's configured schema. +5. Detected entities, classifications, and structured extractions are stored in PostgreSQL. +6. Results are available immediately through the API. + +The underlying model is **DeBERTa-v3-base** fine-tuned for zero-shot NER. It accepts a list of target entity labels and returns spans with confidence scores -- no task-specific training is needed. + +## NER Schemas + +A schema defines which entity types to extract, which document classifications to apply, and what structured fields to pull from the text. Five pre-built schemas ship with Neo. + +### Default Schema + +General-purpose extraction suitable for most document types. + +| Entity Type | Description | +|---|---| +| `person` | Names of people, individuals, or human beings | +| `organization` | Company names, institutions, agencies, or organizations | +| `location` | Geographic locations, cities, countries, addresses | +| `date` | Dates, time periods, or temporal references | +| `money` | Monetary amounts, prices, or financial values | +| `email` | Email addresses | +| `phone` | Phone numbers or contact numbers | +| `url` | Web URLs or links | + +**Classifications:** `document_type` (report, memo, email, contract, invoice, policy, manual, other), `language` (english, spanish, french, german, other) + +**Default confidence threshold:** 0.7 + +### Legal Schema + +Optimized for contracts, agreements, and court filings. + +| Entity Type | Description | +|---|---| +| `party` | Legal parties, signatories, or contracting entities | +| `person` | Names of individuals mentioned in the document | +| `organization` | Company names, law firms, or institutions | +| `date` | Dates, deadlines, or time periods | +| `money` | Monetary amounts, fees, or financial terms | +| `jurisdiction` | Legal jurisdictions, courts, or governing law references | +| `case_number` | Case numbers, docket numbers, or reference numbers | +| `law_reference` | References to laws, statutes, or regulations | + +**Classifications:** `document_type` (contract, agreement, amendment, nda, mou, letter_of_intent, court_filing, legal_opinion, terms_of_service, privacy_policy, other), `contract_status` (draft, pending_signature, executed, expired, terminated) + +**Structured extraction:** `contract_terms` -- parties, effective_date, expiration_date, term_length, renewal, termination_notice, governing_law, total_value + +**Default confidence threshold:** 0.75 + +### Financial Schema + +Tailored for invoices, bank statements, and financial reports. + +| Entity Type | Description | +|---|---| +| `company` | Company names, corporations, or business entities | +| `person` | Names of individuals, executives, or account holders | +| `money` | Monetary amounts, prices, or financial values | +| `percentage` | Percentage values, rates, or ratios | +| `date` | Dates, fiscal periods, or time references | +| `account_number` | Bank account numbers or financial account identifiers | +| `ticker` | Stock ticker symbols | +| `currency` | Currency types or codes | + +**Classifications:** `document_type` (invoice, receipt, bank_statement, financial_report, tax_document, expense_report, purchase_order, quote, other), `transaction_type` (payment, refund, transfer, deposit, withdrawal, fee, other) + +**Structured extraction:** `transaction` (amount, date, description, type, account, reference), `invoice_details` (invoice_number, vendor, customer, subtotal, tax, total, due_date) + +**Default confidence threshold:** 0.8 + +### Healthcare Schema + +Designed for medical records, lab reports, and prescriptions. + +| Entity Type | Description | +|---|---| +| `patient` | Patient names or identifiers | +| `provider` | Healthcare provider names, doctors, or medical staff | +| `organization` | Hospitals, clinics, or healthcare facilities | +| `medication` | Drug names, medications, or pharmaceutical substances | +| `dosage` | Medication dosages, amounts, or frequencies | +| `condition` | Medical conditions, diagnoses, or symptoms | +| `procedure` | Medical procedures, treatments, or interventions | +| `date` | Dates, appointment times, or time references | +| `lab_value` | Laboratory test values or measurements | + +**Classifications:** `document_type` (medical_record, lab_report, prescription, discharge_summary, referral, insurance_claim, consent_form, other), `urgency` (routine, urgent, emergency) + +**Structured extraction:** `patient_info`, `prescription`, `visit_summary` + +**Default confidence threshold:** 0.8 + +### HR Schema + +Built for resumes, offer letters, and employee records. + +| Entity Type | Description | +|---|---| +| `person` | Names of individuals, candidates, or employees | +| `organization` | Company names, employers, or institutions | +| `job_title` | Job titles, positions, or roles | +| `skill` | Skills, competencies, or qualifications | +| `education` | Educational institutions, degrees, or certifications | +| `date` | Dates, employment periods, or time references | +| `location` | Work locations, offices, or addresses | +| `salary` | Salary amounts, compensation, or benefits | +| `email` | Email addresses | +| `phone` | Phone numbers | + +**Classifications:** `document_type` (resume, cover_letter, job_description, offer_letter, performance_review, employee_handbook, policy, other), `experience_level` (entry, mid, senior, executive) + +**Structured extraction:** `candidate_info`, `employment_history`, `education` + +**Default confidence threshold:** 0.7 + +## Configuration + +The NER service is configured through environment variables set on the `ner` container. + +| Variable | Default | Description | +|---|---|---| +| `NER_MODEL_NAME` | `fastino/gliner2-base-v1` | Hugging Face model identifier. The model is downloaded on first startup. | +| `NER_CONFIDENCE_THRESHOLD` | `0.7` | Global minimum confidence score (0.0--1.0). Per-share thresholds override this. | +| `NER_DEVICE` | `auto` | Compute device: `auto`, `cuda`, or `cpu`. `auto` selects CUDA when a GPU is detected. | +| `NER_MAX_TEXT_LENGTH` | `8000` | Maximum characters per chunk sent to the model. GLiNER2 has a 2048-token context window (~4 chars/token = ~8000 chars). | +| `NER_CUDA_MAX_TEXT_LENGTH` | `8000` | Hard cap on chunk size when running on CUDA to prevent out-of-memory errors. | +| `NER_CHUNK_OVERLAP` | `500` | Character overlap between adjacent chunks so entities at boundaries are not missed. | +| `NER_TOKEN_BUDGET` | `2048` | Token budget per chunk for the tokenizer-aware chunking path. | +| `NER_MAX_BATCH_SIZE` | `32` | Upper limit for adaptive batch sizing on GPU. | +| `NER_ESTIMATED_MB_PER_CHUNK` | `1500` | Estimated VRAM (MB) per chunk, used for initial batch size calculation before probing. | +| `NER_CPU_BATCH_SIZE` | `16` | Fixed batch size when running on CPU. | +| `NER_BATCH_RECOVERY_INTERVAL` | `50` | Number of successful inferences before attempting to increase batch size after an OOM. | +| `NER_MAX_CONSECUTIVE_OOMS` | `3` | After this many consecutive OOM errors, the engine falls back to CPU. | +| `NER_CUDA_RETRY_COOLDOWN` | `200` | Number of successful CPU inferences before retrying CUDA after an OOM fallback. | +| `NER_MAX_CUDA_RETRIES` | `3` | Maximum number of times to retry moving back to CUDA after OOM fallback. | + +## Per-Share NER Settings + +NER is enabled and configured per share through the share's rules. When creating or updating a share, include the NER fields in the `rules` object. + +| Rule Field | Type | Default | Description | +|---|---|---|---| +| `enable_ner_analysis` | bool | `false` | Master switch -- set to `true` to run NER on files in this share. | +| `ner_schema` | string | `"default"` | Which pre-built schema to use (`default`, `legal`, `financial`, `healthcare`, `hr`). | +| `ner_entity_types` | list | `["person", "organization", "location", "date", "money"]` | Override the schema's entity types with a custom list. | +| `ner_classifications` | object | `null` | Override classification labels. | +| `ner_structured_extraction` | object | `null` | Override structured extraction fields. | +| `ner_confidence_threshold` | float | `0.7` | Minimum confidence for this share (overrides global and schema defaults). | + +### Example: Creating a Share with Legal NER + +```bash +curl -s -X POST "$NEO_URL/shares" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Legal Contracts", + "path": "/mnt/contracts", + "rules": { + "enable_ner_analysis": true, + "ner_schema": "legal", + "ner_confidence_threshold": 0.75 + } + }' +``` + +### Example: Custom Entity Types (No Schema) + +```bash +curl -s -X POST "$NEO_URL/shares" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Research Papers", + "path": "/mnt/research", + "rules": { + "enable_ner_analysis": true, + "ner_entity_types": ["person", "organization", "date", "location", "chemical_compound", "gene_name"], + "ner_confidence_threshold": 0.65 + } + }' +``` + +## API Endpoints + +All NER endpoints are under `/ner` and require a valid bearer token. + +### List Schemas + +```bash +curl -s "$NEO_URL/ner/schemas" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +Returns all registered schemas with their entity types, classification availability, and structured extraction availability. + +### Get a Specific Schema + +```bash +curl -s "$NEO_URL/ner/schemas/legal" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +### Get NER Results for a File + +```bash +curl -s "$NEO_URL/ner/files/{file_id}" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +Returns entities, classifications, and structured extractions for a single file. + +### Get NER Results for a Share + +```bash +curl -s "$NEO_URL/ner/shares/{share_id}/results?page=1&page_size=50" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +Paginated results across all files in a share. Use `entity_type` to filter: + +```bash +curl -s "$NEO_URL/ner/shares/{share_id}/results?entity_type=person" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +### Global NER Statistics + +```bash +curl -s "$NEO_URL/ner/stats" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +Returns `total_entities`, `total_files_processed`, and a breakdown by `entity_types`. + +### Per-Share NER Statistics + +```bash +curl -s "$NEO_URL/ner/shares/{share_id}/stats" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +### Search Entities + +Search for entities by value across all shares or within a specific share. + +```bash +# Search globally +curl -s "$NEO_URL/ner/entities/search?q=NetApp" \ + -H "Authorization: Bearer $TOKEN" | jq . + +# Filter by entity type and share +curl -s "$NEO_URL/ner/entities/search?q=NetApp&entity_type=organization&share_id={share_id}" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +### Aggregate Entities + +Get aggregated counts of entity values, useful for dashboards and analytics. + +```bash +# All entities +curl -s "$NEO_URL/ner/entities/aggregate" \ + -H "Authorization: Bearer $TOKEN" | jq . + +# Filter by type +curl -s "$NEO_URL/ner/entities/aggregate?entity_type=person&share_id={share_id}&limit=20" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +### Get NER Settings + +```bash +curl -s "$NEO_URL/ner/settings" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +Returns the current global NER configuration: `enabled`, `model`, `batch_size`, `confidence_threshold`, `device`. + +### Update NER Settings + +```bash +curl -s -X PUT "$NEO_URL/ner/settings" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "confidence_threshold": 0.8, + "device": "cuda" + }' | jq . +``` + +Valid `device` values: `auto`, `cuda`, `cpu`. When the device is changed, the API forwards the change to the NER service so the model is moved immediately. + +### Trigger Reanalysis + +Queue all files in a share for NER reprocessing. By default, only files without existing results are processed. Pass `force=true` to reanalyze everything. + +```bash +# Analyze files that are missing NER results +curl -s -X POST "$NEO_URL/ner/shares/{share_id}/reanalyze" \ + -H "Authorization: Bearer $TOKEN" | jq . + +# Force reanalysis of all files +curl -s -X POST "$NEO_URL/ner/shares/{share_id}/reanalyze?force=true" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +### Delete NER Results + +```bash +# Delete results for a single file +curl -s -X DELETE "$NEO_URL/ner/files/{file_id}" \ + -H "Authorization: Bearer $TOKEN" | jq . + +# Delete all results for a share +curl -s -X DELETE "$NEO_URL/ner/shares/{share_id}/results" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +### Check Pending Files + +```bash +curl -s "$NEO_URL/ner/pending?share_id={share_id}&limit=50" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +## GPU Acceleration + +The NER service supports GPU acceleration through NVIDIA CUDA and AMD ROCm. GPU variants are built as separate container images (`netapp-neo-ner-cuda` and `netapp-neo-ner-rocm`). + +### Device Selection + +Set `NER_DEVICE=auto` (the default) to let the engine detect available hardware. It checks for CUDA availability via PyTorch and falls back to CPU if no GPU is found. + +### Text Chunking + +GLiNER2 is based on DeBERTa-v3-base with a 2048-token context window. For documents longer than the context window, the engine splits text into chunks of up to `NER_MAX_TEXT_LENGTH` characters with `NER_CHUNK_OVERLAP` characters of overlap between adjacent chunks. Entity spans detected across chunk boundaries are deduplicated in post-processing. + +### Adaptive Batch Sizing + +On GPU, the engine probes available VRAM at startup and computes an initial batch size. During inference: + +- Successful batches gradually increase the batch size up to `NER_MAX_BATCH_SIZE`. +- An OOM error halves the batch size and retries. +- After `NER_MAX_CONSECUTIVE_OOMS` consecutive OOM errors, the engine falls back to CPU automatically. +- After `NER_CUDA_RETRY_COOLDOWN` successful CPU inferences, the engine attempts to move back to CUDA (up to `NER_MAX_CUDA_RETRIES` times). + +### VRAM Requirements + +The GLiNER2 base model requires approximately 500 MB of VRAM. Each inference chunk uses an estimated `NER_ESTIMATED_MB_PER_CHUNK` MB (default 1500 MB) for activations and gradients. A GPU with 4 GB of VRAM can comfortably run batch size 1--2; 8 GB or more is recommended for larger batch sizes. + +## Troubleshooting + +### Model Download Fails on First Startup + +The NER service downloads the GLiNER2 model from Hugging Face on first launch. If the container has no internet access, pre-download the model and mount it into the container: + +```bash +# On a machine with internet access +python3 -c "from gliner2 import GLiNER2; GLiNER2.from_pretrained('fastino/gliner2-base-v1')" + +# The model is cached in ~/.cache/huggingface/hub/ +# Mount that directory into the container +docker run -v ~/.cache/huggingface:/root/.cache/huggingface ... +``` + +Alternatively, set `NER_MODEL_NAME` to a local path where the model weights are mounted. + +### CUDA Out-of-Memory (OOM) Errors + +Symptoms: log messages containing `CUDA out of memory` or `RuntimeError: CUDA error`. + +Actions: +- Reduce `NER_CUDA_MAX_TEXT_LENGTH` to send smaller chunks (try `4000`). +- Reduce `NER_MAX_BATCH_SIZE` to `1`. +- Increase `NER_MAX_CONSECUTIVE_OOMS` if you want the engine to tolerate more OOMs before falling back. +- If the GPU has limited VRAM (less than 4 GB), set `NER_DEVICE=cpu` to avoid OOM entirely. + +The engine automatically falls back to CPU after repeated OOMs and will attempt to return to CUDA after a cooldown period. + +### Tokenizer Deadlock + +The Hugging Face `tokenizers` library uses Rust-based parallelism that can deadlock when called from multiple Python threads inside a forked process. Symptoms: the NER service hangs during inference with no log output. + +Workaround: Set the environment variable `TOKENIZERS_PARALLELISM=false` on the NER container. This disables tokenizer-level parallelism and prevents the deadlock. The Neo container images set this by default. + +### NER Results Are Empty + +Check that: +1. The share has `enable_ner_analysis: true` in its rules. +2. The file has completed text extraction (status = `completed`). +3. The confidence threshold is not set too high -- try lowering `ner_confidence_threshold` to `0.5` temporarily. +4. The NER service is running and reachable from the worker service (check `GET /ner/status`). + +### Reprocessing After Schema Change + +If you change a share's NER schema or entity types, existing results are not automatically updated. Use the reanalyze endpoint with `force=true` to reprocess all files with the new configuration: + +```bash +curl -s -X POST "$NEO_URL/ner/shares/{share_id}/reanalyze?force=true" \ + -H "Authorization: Bearer $TOKEN" +``` \ No newline at end of file diff --git a/neocoreapi/openapi.json b/neocoreapi/openapi.json new file mode 100644 index 0000000..659cb8a --- /dev/null +++ b/neocoreapi/openapi.json @@ -0,0 +1 @@ +{"openapi":"3.1.0","info":{"title":"NetApp API Service","description":"Lightweight API service for NetApp Project Neo","version":"4.1.2-aide.1"},"paths":{"/health":{"get":{"tags":["Health"],"summary":"Health Check","description":"Health check endpoint for load balancers and orchestration.","operationId":"health_check_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health/detailed":{"get":{"tags":["Health"],"summary":"Detailed Health","description":"Detailed health check with component status.","operationId":"detailed_health_health_detailed_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/ready":{"get":{"tags":["Health"],"summary":"Readiness Check","description":"Readiness check for Kubernetes.","operationId":"readiness_check_ready_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/version":{"get":{"tags":["System"],"summary":"Get Version","description":"Return the current version information of the connector.","operationId":"get_version_version_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/license/status":{"get":{"tags":["System"],"summary":"License Status","description":"Check the current license status.\n\nReturns license validity and details without requiring authentication.","operationId":"license_status_api_v1_license_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/auth/providers":{"get":{"tags":["Authentication"],"summary":"Get Auth Providers","description":"Get available authentication providers.","operationId":"get_auth_providers_api_v1_auth_providers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/token":{"post":{"tags":["MCP"],"summary":"OAuth Token Endpoint","description":"Exchanges authorization code for tokens via Entra ID, or authenticates local users","operationId":"token_endpoint_token_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/logout":{"post":{"tags":["Authentication"],"summary":"Logout","description":"Logout endpoint.\n\nNote: JWT tokens are stateless, so logout is handled client-side.\nThis endpoint exists for API completeness.","operationId":"logout_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/shares":{"get":{"tags":["Shares"],"summary":"List Shares","description":"List configured shares.\n\nDefault behaviour returns every share (matches the pre-4.1.1 contract). At\nhigh tenancy fan-out this response can be many MB; pass ``?limit=N`` (and\noptionally ``?offset=M``) to bound it.","operationId":"list_shares_api_v1_shares_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":10000,"minimum":1},{"type":"null"}],"description":"Maximum shares to return; omit for the full list","title":"Limit"},"description":"Maximum shares to return; omit for the full list"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Number of shares to skip","default":0,"title":"Offset"},"description":"Number of shares to skip"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ShareResponse"},"title":"Response List Shares Api V1 Shares Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Shares"],"summary":"Create Share","description":"Create a new share configuration.\n\nThe share will be tested for connectivity before being saved.\nIf crawl_immediately is True and connection succeeds, a crawl will be triggered.","operationId":"create_share_api_v1_shares_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"crawl_immediately","in":"query","required":false,"schema":{"type":"boolean","description":"Trigger immediate crawl after creation","default":false,"title":"Crawl Immediately"},"description":"Trigger immediate crawl after creation"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareConfig"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/shares/{share_id}":{"get":{"tags":["Shares"],"summary":"Get Share","description":"Get a specific share by ID.","operationId":"get_share_api_v1_shares__share_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Shares"],"summary":"Update Share","description":"Update a share configuration.\n\nIf enable_copilot_upload setting changes:\n- Enabled: Triggers backfill to upload pre-crawled items to Graph\n- Disabled: Triggers cleanup to remove items from Graph","operationId":"update_share_api_v1_shares__share_id__patch","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Shares"],"summary":"Delete Share","description":"Delete a share and trigger cleanup.\n\nThis marks the share for deletion and creates a cleanup work item\nfor the worker service to clean up Graph entries.","operationId":"delete_share_api_v1_shares__share_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/shares/{share_id}/crawl":{"post":{"tags":["Shares"],"summary":"Trigger Crawl","description":"Trigger a crawl for a share.\n\nThis creates an enumeration work item for the worker service.","operationId":"trigger_crawl_api_v1_shares__share_id__crawl_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}},{"name":"force","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Force"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/shares/{share_id}/recrawl-missing-content":{"post":{"tags":["Shares"],"summary":"Recrawl Missing Content","description":"Re-queue extraction for files that are missing content.\n\nCovers two cases:\n1. file_metadata exists with NULL content — deletes stale metadata and re-queues.\n2. file_inventory entries stuck in 'discovered' with no metadata at all — queues extraction.","operationId":"recrawl_missing_content_api_v1_shares__share_id__recrawl_missing_content_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/shares/{share_id}/progress":{"get":{"tags":["Shares"],"summary":"Get Crawl Progress","description":"Get crawl progress for a share.","operationId":"get_crawl_progress_api_v1_shares__share_id__progress_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/shares/{share_id}/test-connection":{"post":{"tags":["Shares"],"summary":"Test Share Connection","description":"Test connection to an existing share.","operationId":"test_share_connection_api_v1_shares__share_id__test_connection_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/shares/{share_id}/graph/status":{"get":{"tags":["Shares","Graph Sync"],"summary":"Get Graph Sync Status","description":"Get Graph sync status for a share.\n\nReturns counts of:\n- pending_upload: Files extracted but not yet uploaded to Graph\n- uploaded: Files successfully uploaded to Graph\n- failed: Upload work items that permanently failed\n- in_progress: Upload work items currently being processed","operationId":"get_graph_sync_status_api_v1_shares__share_id__graph_status_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/shares/{share_id}/graph/backfill":{"post":{"tags":["Shares","Graph Sync"],"summary":"Backfill Graph Uploads","description":"Manually trigger Graph backfill for pre-crawled items.\n\nThis creates upload work items for all extracted files that haven't\nbeen uploaded to Microsoft Graph yet. Useful when:\n- Graph upload was disabled during initial crawl\n- You want to re-upload files that were skipped\n- Recovering from a failed Graph connector configuration","operationId":"backfill_graph_uploads_api_v1_shares__share_id__graph_backfill_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/shares/{share_id}/graph/cleanup":{"post":{"tags":["Shares","Graph Sync"],"summary":"Cleanup Graph Uploads","description":"Manually trigger Graph cleanup for uploaded items.\n\nThis creates cleanup work items to remove all previously uploaded\nfiles from Microsoft Graph. Use this when:\n- Removing a share's content from Graph\n- Cleaning up after disabling Graph upload\n- Resetting Graph state for a share","operationId":"cleanup_graph_uploads_api_v1_shares__share_id__graph_cleanup_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/shares/{share_id}/graph/retry-failed":{"post":{"tags":["Shares","Graph Sync"],"summary":"Retry Failed Uploads","description":"Re-queue failed Graph upload work items for retry.\n\nThis resets all failed upload work items back to pending status,\nclearing their retry count so they can be processed again. Use when:\n- Network issues caused temporary upload failures\n- Graph API was temporarily unavailable\n- Configuration issues have been resolved","operationId":"retry_failed_uploads_api_v1_shares__share_id__graph_retry_failed_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/shares/{share_id}/graph/force-reupload":{"post":{"tags":["Shares","Graph Sync"],"summary":"Force Reupload","description":"Force re-upload of completed Graph upload work items.\n\nThis resets all completed upload work items back to pending status so they\nwill be re-processed. Use after changing share settings (e.g. acl_override_mode)\nthat require files to be re-uploaded to Graph.","operationId":"force_reupload_api_v1_shares__share_id__graph_force_reupload_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/shares/{share_id}/files":{"get":{"tags":["Files"],"summary":"List Files","description":"List files in a share directory.\n\nSupports OFFSET pagination (``page`` + ``page_size``) and keyset pagination\n(``after_modified_time``). For shares with millions of files, prefer keyset.","operationId":"list_files_api_v1_shares__share_id__files_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}},{"name":"path","in":"query","required":false,"schema":{"type":"string","description":"Directory path to list","default":"/","title":"Path"},"description":"Directory path to list"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (ignored when after_modified_time is set)","default":1,"title":"Page"},"description":"Page number (ignored when after_modified_time is set)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Items per page","default":100,"title":"Page Size"},"description":"Items per page"},{"name":"fields","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated field names or '*' for all","title":"Fields"},"description":"Comma-separated field names or '*' for all"},{"name":"field_set","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Predefined field set (minimal, standard, metadata, security, full)","title":"Field Set"},"description":"Predefined field set (minimal, standard, metadata, security, full)"},{"name":"include_content","in":"query","required":false,"schema":{"type":"boolean","description":"Include file content in response","default":false,"title":"Include Content"},"description":"Include file content in response"},{"name":"include_counts","in":"query","required":false,"schema":{"type":"boolean","description":"Include total_count + total_size; set false at billion-row scale to avoid the cross-partition aggregate","default":true,"title":"Include Counts"},"description":"Include total_count + total_size; set false at billion-row scale to avoid the cross-partition aggregate"},{"name":"after_modified_time","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Keyset cursor: returns rows with modified_time strictly less than this value, ordered by modified_time DESC. ~300x faster than OFFSET pagination at deep pages.","title":"After Modified Time"},"description":"Keyset cursor: returns rows with modified_time strictly less than this value, ordered by modified_time DESC. ~300x faster than OFFSET pagination at deep pages."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/shares/{share_id}/files/metadata":{"get":{"tags":["Files"],"summary":"Get File Metadata","description":"Get metadata for a specific file in a share (legacy UI compatibility).\n\nSupports lookup by file_id or path query parameter.","operationId":"get_file_metadata_api_v1_shares__share_id__files_metadata_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}},{"name":"path","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Path to the file","title":"Path"},"description":"Path to the file"},{"name":"file_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ID of the file","title":"File Id"},"description":"ID of the file"},{"name":"fields","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated field names or '*' for all","title":"Fields"},"description":"Comma-separated field names or '*' for all"},{"name":"field_set","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Predefined field set (minimal, standard, metadata, security, full)","title":"Field Set"},"description":"Predefined field set (minimal, standard, metadata, security, full)"},{"name":"include_content","in":"query","required":false,"schema":{"type":"boolean","description":"Include file content","default":true,"title":"Include Content"},"description":"Include file content"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/shares/{share_id}/files/{file_id}":{"get":{"tags":["Files"],"summary":"Get File","description":"Get a specific file by ID.","operationId":"get_file_api_v1_shares__share_id__files__file_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}},{"name":"file_id","in":"path","required":true,"schema":{"type":"string","title":"File Id"}},{"name":"fields","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated field names or '*' for all","title":"Fields"},"description":"Comma-separated field names or '*' for all"},{"name":"field_set","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Predefined field set (minimal, standard, metadata, security, full)","title":"Field Set"},"description":"Predefined field set (minimal, standard, metadata, security, full)"},{"name":"include_content","in":"query","required":false,"schema":{"type":"boolean","description":"Include file content","default":true,"title":"Include Content"},"description":"Include file content"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/files/{file_id}":{"get":{"tags":["Files"],"summary":"Get File By Id","description":"Get a specific file by ID (direct lookup, no share_id required).\n\nThis endpoint is used by MCP tools to retrieve file content after search.","operationId":"get_file_by_id_api_v1_files__file_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"file_id","in":"path","required":true,"schema":{"type":"string","title":"File Id"}},{"name":"fields","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated field names","title":"Fields"},"description":"Comma-separated field names"},{"name":"field_set","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Predefined field set (minimal, standard, metadata, security, full)","title":"Field Set"},"description":"Predefined field set (minimal, standard, metadata, security, full)"},{"name":"include_content","in":"query","required":false,"schema":{"type":"boolean","description":"Include file content","default":true,"title":"Include Content"},"description":"Include file content"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/files":{"get":{"tags":["Files"],"summary":"List All Files","description":"List files across all shares.\n\nSupports pagination (OFFSET via ``page``, or keyset via\n``after_modified_time``), field selection, file type filtering, and\ncase-insensitive filename substring search.","operationId":"list_all_files_api_v1_files_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (ignored when after_modified_time is set)","default":1,"title":"Page"},"description":"Page number (ignored when after_modified_time is set)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Items per page","default":100,"title":"Page Size"},"description":"Items per page"},{"name":"fields","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated field names","title":"Fields"},"description":"Comma-separated field names"},{"name":"field_set","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Predefined field set","title":"Field Set"},"description":"Predefined field set"},{"name":"file_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by file type","title":"File Type"},"description":"Filter by file type"},{"name":"filename","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Case-insensitive filename substring filter","title":"Filename"},"description":"Case-insensitive filename substring filter"},{"name":"include_counts","in":"query","required":false,"schema":{"type":"boolean","description":"Include total_count + total_size; set false at billion-row scale to avoid the cross-partition aggregate","default":true,"title":"Include Counts"},"description":"Include total_count + total_size; set false at billion-row scale to avoid the cross-partition aggregate"},{"name":"after_modified_time","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Keyset cursor: returns rows with modified_time strictly less than this value, ordered by modified_time DESC. ~300x faster than OFFSET pagination at deep pages.","title":"After Modified Time"},"description":"Keyset cursor: returns rows with modified_time strictly less than this value, ordered by modified_time DESC. ~300x faster than OFFSET pagination at deep pages."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AllSharesFileListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/search":{"post":{"tags":["Files"],"summary":"Full Text Search","description":"Full-text search across file content.\n\nSearches file content using database-native full-text search.\nDelegates to Database.full_text_search() which uses GIN-indexed search_vector\ncolumn (PostgreSQL) or FULLTEXT indexes (MySQL) for optimal performance.","operationId":"full_text_search_api_v1_search_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FullTextSearchRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FullTextSearchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/users/me":{"get":{"tags":["Users"],"summary":"Get Current User Info","description":"Get current user's information.","operationId":"get_current_user_info_api_v1_users_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/users/me/password":{"patch":{"tags":["Users"],"summary":"Change Password","description":"Change current user's password.","operationId":"change_password_api_v1_users_me_password_patch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordChange"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/users":{"get":{"tags":["Users"],"summary":"List Users","description":"List all users (admin only).","operationId":"list_users_api_v1_users_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/UserResponse"},"type":"array","title":"Response List Users Api V1 Users Get"}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"post":{"tags":["Users"],"summary":"Create User","description":"Create a new user (admin only).","operationId":"create_user_api_v1_users_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/users/{user_id}":{"get":{"tags":["Users"],"summary":"Get User","description":"Get a specific user (admin only).","operationId":"get_user_api_v1_users__user_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Users"],"summary":"Update User","description":"Update a user (admin only).","operationId":"update_user_api_v1_users__user_id__patch","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Users"],"summary":"Delete User","description":"Delete a user (admin only).","operationId":"delete_user_api_v1_users__user_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/users/admin":{"post":{"tags":["Users"],"summary":"Create Admin User","description":"Create a new admin user.","operationId":"create_admin_user_api_v1_users_admin_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/monitoring/overview":{"get":{"tags":["Monitoring"],"summary":"Get Monitoring Overview","description":"Get comprehensive monitoring overview.","operationId":"get_monitoring_overview_api_v1_monitoring_overview_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MonitoringOverviewResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/monitoring/work-queue":{"get":{"tags":["Monitoring"],"summary":"Get Work Queue Stats","description":"Get detailed work queue statistics.","operationId":"get_work_queue_stats_api_v1_monitoring_work_queue_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/monitoring/work-queue/by-share/{share_id}":{"get":{"tags":["Monitoring"],"summary":"Get Share Work Queue Stats","description":"Get work queue statistics for a specific share.","operationId":"get_share_work_queue_stats_api_v1_monitoring_work_queue_by_share__share_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/monitoring/workers":{"get":{"tags":["Monitoring"],"summary":"Get Worker Stats","description":"Get statistics about active workers.","operationId":"get_worker_stats_api_v1_monitoring_workers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/monitoring/services":{"get":{"tags":["Monitoring"],"summary":"Get Service Health","description":"Get health status of all services.","operationId":"get_service_health_api_v1_monitoring_services_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/monitoring/failed-items":{"get":{"tags":["Monitoring"],"summary":"Get Failed Items","description":"Get list of failed work items for troubleshooting.","operationId":"get_failed_items_api_v1_monitoring_failed_items_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Share Id"}},{"name":"work_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Work Type"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/monitoring/retry-failed":{"post":{"tags":["Monitoring"],"summary":"Retry Failed Items","description":"Retry failed work items.","operationId":"retry_failed_items_api_v1_monitoring_retry_failed_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Share Id"}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_retry_failed_items_api_v1_monitoring_retry_failed_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/monitoring/operations":{"get":{"tags":["Monitoring"],"summary":"Get Operations","description":"Get operation logs with optional filtering.\n\nFilters:\n - type: operation type\n - action: action (from metadata)\n - status: operation status\n - share_id: share ID (from metadata)\n - since: ISO timestamp (only results after this time)\n - limit: max results","operationId":"get_operations_api_v1_monitoring_operations_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by operation type","title":"Type"},"description":"Filter by operation type"},{"name":"action","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by action (metadata)","title":"Action"},"description":"Filter by action (metadata)"},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by status","title":"Status"},"description":"Filter by status"},{"name":"share_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by share ID (metadata)","title":"Share Id"},"description":"Filter by share ID (metadata)"},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Limit to operations since this ISO timestamp","title":"Since"},"description":"Limit to operations since this ISO timestamp"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","description":"Max number of results to return","default":100,"title":"Limit"},"description":"Max number of results to return"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OperationLog"},"title":"Response Get Operations Api V1 Monitoring Operations Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/monitoring/database/size":{"get":{"tags":["Monitoring"],"summary":"Get Database Size","description":"Get comprehensive database size and statistics information.\n\nReturns detailed information about:\n- Database file size on disk\n- Table statistics (row counts)\n- Total file content size stored\n- Timestamp of the measurement","operationId":"get_database_size_api_v1_monitoring_database_size_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/monitoring/enumeration":{"get":{"tags":["Monitoring"],"summary":"Get Enumeration Stats","description":"Get enumeration stats from worker service.","operationId":"get_enumeration_stats_api_v1_monitoring_enumeration_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/monitoring/graph-rate-limit":{"get":{"tags":["Monitoring"],"summary":"Get Graph Rate Limit Stats","description":"Get Graph API rate limit stats from worker service.","operationId":"get_graph_rate_limit_stats_api_v1_monitoring_graph_rate_limit_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/monitoring/work-items/retry":{"post":{"tags":["Monitoring"],"summary":"Retry Work Items","description":"Retry failed work items.","operationId":"retry_work_items_api_v1_monitoring_work_items_retry_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Share Id"}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_retry_work_items_api_v1_monitoring_work_items_retry_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/monitoring/sizing/profiles":{"get":{"tags":["Monitoring"],"summary":"Get Sizing Profiles","description":"Get all sizing profiles with recommended configurations.","operationId":"get_sizing_profiles_api_v1_monitoring_sizing_profiles_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/monitoring/sizing/current":{"get":{"tags":["Monitoring"],"summary":"Get Current Sizing","description":"Get current config vs. recommended sizing profile.","operationId":"get_current_sizing_api_v1_monitoring_sizing_current_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/monitoring/sizing/parameters":{"get":{"tags":["Monitoring"],"summary":"Get Sizing Parameters","description":"Get all tunable parameters with descriptions, defaults, and current values.","operationId":"get_sizing_parameters_api_v1_monitoring_sizing_parameters_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/monitoring/benchmark/run":{"post":{"tags":["Monitoring"],"summary":"Start Benchmark","description":"Start a benchmark run on the worker service.","operationId":"start_benchmark_api_v1_monitoring_benchmark_run_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Share Id"}},{"name":"sample_size","in":"query","required":false,"schema":{"type":"integer","default":100,"title":"Sample Size"}},{"name":"stages","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stages"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/monitoring/benchmark/status":{"get":{"tags":["Monitoring"],"summary":"Get Benchmark Status","description":"Get current benchmark progress.","operationId":"get_benchmark_status_api_v1_monitoring_benchmark_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/monitoring/benchmark/results":{"get":{"tags":["Monitoring"],"summary":"Get Benchmark Results","description":"Get the latest benchmark results.","operationId":"get_benchmark_results_api_v1_monitoring_benchmark_results_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/monitoring/benchmark/history":{"get":{"tags":["Monitoring"],"summary":"Get Benchmark History","description":"Get historical benchmark results.","operationId":"get_benchmark_history_api_v1_monitoring_benchmark_history_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/monitoring/tuning/recommendations":{"get":{"tags":["Monitoring"],"summary":"Get Tuning Recommendations","description":"Get current auto-tuner recommendations.","operationId":"get_tuning_recommendations_api_v1_monitoring_tuning_recommendations_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/monitoring/tuning/history":{"get":{"tags":["Monitoring"],"summary":"Get Tuning History","description":"Get history of applied tuning changes.","operationId":"get_tuning_history_api_v1_monitoring_tuning_history_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/monitoring/tuning/apply":{"post":{"tags":["Monitoring"],"summary":"Apply Tuning","description":"Manually apply a tuning recommendation.","operationId":"apply_tuning_api_v1_monitoring_tuning_apply_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"parameter","in":"query","required":true,"schema":{"type":"string","title":"Parameter"}},{"name":"value","in":"query","required":true,"schema":{"type":"string","title":"Value"}},{"name":"reason","in":"query","required":false,"schema":{"type":"string","default":"manual","title":"Reason"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/monitoring/tuning/rollback":{"post":{"tags":["Monitoring"],"summary":"Rollback Tuning","description":"Revert the most recent tuning change.","operationId":"rollback_tuning_api_v1_monitoring_tuning_rollback_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/monitoring/tuning/status":{"get":{"tags":["Monitoring"],"summary":"Get Tuning Status","description":"Get auto-tuner status.","operationId":"get_tuning_status_api_v1_monitoring_tuning_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/ner/files/{file_id}":{"get":{"tags":["NER"],"summary":"Get File Ner Results","description":"Get NER results for a specific file.","operationId":"get_file_ner_results_api_v1_ner_files__file_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"file_id","in":"path","required":true,"schema":{"type":"string","title":"File Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NERResultResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["NER"],"summary":"Delete File Ner Results","description":"Delete NER results for a specific file.","operationId":"delete_file_ner_results_api_v1_ner_files__file_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"file_id","in":"path","required":true,"schema":{"type":"string","title":"File Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/ner/shares/{share_id}/results":{"get":{"tags":["NER"],"summary":"Get Share Ner Results","description":"Get NER results for all files in a share.","operationId":"get_share_ner_results_api_v1_ner_shares__share_id__results_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":100,"title":"Page Size"}},{"name":"entity_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entity Type"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["NER"],"summary":"Delete Share Ner Results","description":"Delete all NER results for a share.","operationId":"delete_share_ner_results_api_v1_ner_shares__share_id__results_delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/ner/stats":{"get":{"tags":["NER"],"summary":"Get Ner Stats","description":"Get overall NER statistics.","operationId":"get_ner_stats_api_v1_ner_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NERStatsResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/ner/shares/{share_id}/stats":{"get":{"tags":["NER"],"summary":"Get Share Ner Stats","description":"Get NER statistics for a specific share.","operationId":"get_share_ner_stats_api_v1_ner_shares__share_id__stats_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NERStatsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/ner/status":{"get":{"tags":["NER"],"summary":"Get Ner Status","description":"Get NER processing status.","operationId":"get_ner_status_api_v1_ner_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/ner/schemas":{"get":{"tags":["NER"],"summary":"Get Ner Schemas","description":"Get available NER schemas.","operationId":"get_ner_schemas_api_v1_ner_schemas_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NERSchemaListResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/ner/schemas/{schema_name}":{"get":{"tags":["NER"],"summary":"Get Ner Schema","description":"Get a specific NER schema.","operationId":"get_ner_schema_api_v1_ner_schemas__schema_name__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"schema_name","in":"path","required":true,"schema":{"type":"string","title":"Schema Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/ner/pending":{"get":{"tags":["NER"],"summary":"Get Pending Ner","description":"Get files pending NER processing.","operationId":"get_pending_ner_api_v1_ner_pending_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Share Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":100,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/ner/entities/aggregate":{"get":{"tags":["NER"],"summary":"Get Entity Aggregates","description":"Get aggregated entity statistics.\n\nBacked by the ner_entity_aggregates rollup table (v4.3+) which gives\nO(1) scan cost regardless of ner_entities row count. Falls back to a\nlive GROUP BY on ner_entities if the rollup is not yet available.","operationId":"get_entity_aggregates_api_v1_ner_entities_aggregate_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"entity_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entity Type"}},{"name":"share_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Share Id"}},{"name":"share_ids","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated share IDs","title":"Share Ids"},"description":"Comma-separated share IDs"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/ner/entities/count":{"get":{"tags":["NER"],"summary":"Count Entity Mentions","description":"Document-count answer for \"how many files mention X\" questions.\n\nBacked by the ner_entity_aggregates rollup — a single index lookup for\nexact match, trivial regardless of underlying ner_entities cardinality.\nReturns approximate_document_count; the approximation is share-level,\nso counts can exceed what the caller can see when per-file ACLs\ndiverge within a share. See docs/design/rls-migration-design.md.","operationId":"count_entity_mentions_api_v1_ner_entities_count_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","description":"Entity value to count","title":"Q"},"description":"Entity value to count"},{"name":"entity_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by entity type","title":"Entity Type"},"description":"Filter by entity type"},{"name":"share_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Restrict to a single share","title":"Share Id"},"description":"Restrict to a single share"},{"name":"share_ids","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated share IDs","title":"Share Ids"},"description":"Comma-separated share IDs"},{"name":"match_mode","in":"query","required":false,"schema":{"type":"string","description":"exact | prefix | substring","default":"exact","title":"Match Mode"},"description":"exact | prefix | substring"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/ner/entities/search":{"get":{"tags":["NER"],"summary":"Search Entities","description":"Cursor-paginated entity search backed by the denormalised ner_entities table.\n\nReturns {results, count, next_cursor, match_mode, query, entity_type}.\nBacked by partition pruning + trigram GIN (substring) or btree (exact/prefix).","operationId":"search_entities_api_v1_ner_entities_search_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","description":"Entity value to search","title":"Q"},"description":"Entity value to search"},{"name":"entity_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by entity type","title":"Entity Type"},"description":"Filter by entity type"},{"name":"share_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Restrict to a single share","title":"Share Id"},"description":"Restrict to a single share"},{"name":"share_ids","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated share IDs to restrict to","title":"Share Ids"},"description":"Comma-separated share IDs to restrict to"},{"name":"match_mode","in":"query","required":false,"schema":{"type":"string","description":"substring | exact | prefix","default":"substring","title":"Match Mode"},"description":"substring | exact | prefix"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"Page size","default":20,"title":"Limit"},"description":"Page size"},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque cursor from previous response","title":"Cursor"},"description":"Opaque cursor from previous response"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/ner/settings":{"get":{"tags":["NER"],"summary":"Get Ner Settings","description":"Get NER processing settings.","operationId":"get_ner_settings_api_v1_ner_settings_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"put":{"tags":["NER"],"summary":"Update Ner Settings","description":"Update NER processing settings.\n\nWhen the device setting is changed, the update is forwarded to the\nNER service so the model is moved to the new device immediately.","operationId":"update_ner_settings_api_v1_ner_settings_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NERSettingsUpdate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/ner/shares/{share_id}/reanalyze":{"post":{"tags":["NER"],"summary":"Trigger Share Reanalysis","description":"Trigger NER reanalysis for all files in a share.\n\nCreates work queue items for each file that needs NER processing.\nThe worker service picks up these items and performs the analysis.\n\nArgs:\n share_id: The share to reanalyze\n force: If True, reanalyze all files. If False, only analyze files without NER results.","operationId":"trigger_share_reanalysis_api_v1_ner_shares__share_id__reanalyze_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}},{"name":"force","in":"query","required":false,"schema":{"type":"boolean","description":"Force reanalysis even if NER results exist","default":false,"title":"Force"},"description":"Force reanalysis even if NER results exist"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/datasets":{"post":{"tags":["datasets"],"summary":"Create Dataset","description":"Create a virtual dataset from file IDs.","operationId":"create_dataset_api_v1_datasets_post","security":[{"OAuth2PasswordBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDatasetRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DatasetResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["datasets"],"summary":"List Datasets","description":"List datasets accessible to the current user.","operationId":"list_datasets_api_v1_datasets_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Page Size"}},{"name":"owned_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only show datasets owned by the current user","default":false,"title":"Owned Only"},"description":"Only show datasets owned by the current user"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DatasetListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/datasets/expiring":{"get":{"tags":["datasets"],"summary":"List Expiring Datasets","description":"List datasets nearing expiration (admin only).","operationId":"list_expiring_datasets_api_v1_datasets_expiring_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"within_hours","in":"query","required":false,"schema":{"type":"integer","maximum":8760,"minimum":1,"description":"Show datasets expiring within N hours","default":240,"title":"Within Hours"},"description":"Show datasets expiring within N hours"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DatasetExpirationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/datasets/{dataset_id}":{"get":{"tags":["datasets"],"summary":"Get Dataset","description":"Get dataset details.","operationId":"get_dataset_api_v1_datasets__dataset_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"dataset_id","in":"path","required":true,"schema":{"type":"string","title":"Dataset Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DatasetResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["datasets"],"summary":"Update Dataset","description":"Update dataset metadata (owner or admin only).","operationId":"update_dataset_api_v1_datasets__dataset_id__patch","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"dataset_id","in":"path","required":true,"schema":{"type":"string","title":"Dataset Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDatasetRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DatasetResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["datasets"],"summary":"Delete Dataset","description":"Delete a dataset (owner only).","operationId":"delete_dataset_api_v1_datasets__dataset_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"dataset_id","in":"path","required":true,"schema":{"type":"string","title":"Dataset Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/datasets/{dataset_id}/items":{"post":{"tags":["datasets"],"summary":"Add Items","description":"Add files to a dataset (write permission required).","operationId":"add_items_api_v1_datasets__dataset_id__items_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"dataset_id","in":"path","required":true,"schema":{"type":"string","title":"Dataset Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddItemsRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Add Items Api V1 Datasets Dataset Id Items Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["datasets"],"summary":"List Items","description":"List files in a dataset. Applies ACL filtering unless override is enabled.","operationId":"list_items_api_v1_datasets__dataset_id__items_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"dataset_id","in":"path","required":true,"schema":{"type":"string","title":"Dataset Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":100,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DatasetItemsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["datasets"],"summary":"Remove Items","description":"Remove files from a dataset (write permission required).","operationId":"remove_items_api_v1_datasets__dataset_id__items_delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"dataset_id","in":"path","required":true,"schema":{"type":"string","title":"Dataset Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveItemsRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Remove Items Api V1 Datasets Dataset Id Items Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/datasets/{dataset_id}/search":{"post":{"tags":["datasets"],"summary":"Search Dataset","description":"Full-text search within a dataset.","operationId":"search_dataset_api_v1_datasets__dataset_id__search_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"dataset_id","in":"path","required":true,"schema":{"type":"string","title":"Dataset Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DatasetSearchRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DatasetSearchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/datasets/{dataset_id}/ner-search":{"post":{"tags":["datasets"],"summary":"Ner Search Dataset","description":"Cursor-paginated NER entity search scoped to a dataset via SQL JOIN.\n\nUses the new ner_entities table with trigram GIN (substring) or btree\n(exact/prefix) indexes. Partition pruning keeps this fast even at\nbillions of total entities.","operationId":"ner_search_dataset_api_v1_datasets__dataset_id__ner_search_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"dataset_id","in":"path","required":true,"schema":{"type":"string","title":"Dataset Id"}},{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":1,"description":"Entity search term","title":"Q"},"description":"Entity search term"},{"name":"entity_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by entity type","title":"Entity Type"},"description":"Filter by entity type"},{"name":"match_mode","in":"query","required":false,"schema":{"type":"string","description":"substring | exact | prefix","default":"substring","title":"Match Mode"},"description":"substring | exact | prefix"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"Page size","default":20,"title":"Limit"},"description":"Page size"},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque cursor from previous response","title":"Cursor"},"description":"Opaque cursor from previous response"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Ner Search Dataset Api V1 Datasets Dataset Id Ner Search Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/datasets/{dataset_id}/subset":{"post":{"tags":["datasets"],"summary":"Create Subset","description":"Create a new dataset from a subset of an existing dataset's items.","operationId":"create_subset_api_v1_datasets__dataset_id__subset_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"dataset_id","in":"path","required":true,"schema":{"type":"string","title":"Dataset Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSubsetRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DatasetResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/datasets/{dataset_id}/shares":{"post":{"tags":["datasets"],"summary":"Share Dataset","description":"Share a dataset with a user or group (admin/owner required).","operationId":"share_dataset_api_v1_datasets__dataset_id__shares_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"dataset_id","in":"path","required":true,"schema":{"type":"string","title":"Dataset Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareDatasetRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DatasetShareResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["datasets"],"summary":"List Shares","description":"List all shares for a dataset (admin/owner required).","operationId":"list_shares_api_v1_datasets__dataset_id__shares_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"dataset_id","in":"path","required":true,"schema":{"type":"string","title":"Dataset Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DatasetShareResponse"},"title":"Response List Shares Api V1 Datasets Dataset Id Shares Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/datasets/{dataset_id}/shares/{share_id}":{"patch":{"tags":["datasets"],"summary":"Update Share","description":"Update a dataset share's permission or expiration.","operationId":"update_share_api_v1_datasets__dataset_id__shares__share_id__patch","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"dataset_id","in":"path","required":true,"schema":{"type":"string","title":"Dataset Id"}},{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}},{"name":"permission","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"New permission level","title":"Permission"},"description":"New permission level"},{"name":"expires_at","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"New expiration date","title":"Expires At"},"description":"New expiration date"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DatasetShareResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["datasets"],"summary":"Revoke Share","description":"Revoke a dataset share (admin/owner required).","operationId":"revoke_share_api_v1_datasets__dataset_id__shares__share_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"dataset_id","in":"path","required":true,"schema":{"type":"string","title":"Dataset Id"}},{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/setup/status":{"get":{"tags":["setup"],"summary":"Get Setup Status","description":"Get current setup status.\n\nReturns setup completion status and list of completed/required steps.\nAlso indicates if the application is in license reconfiguration mode.","operationId":"get_setup_status_api_v1_setup_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetupStatusResponse"}}}}}}},"/api/v1/setup/complete":{"post":{"tags":["setup"],"summary":"Complete Setup","description":"Mark setup as complete.\n\nCall this endpoint after all required configuration steps are finished\nto exit setup mode and start normal operation.\n\n**Prerequisites**:\n- DATABASE_URL environment variable must be set\n- License must be configured (via API or NETAPP_CONNECTOR_LICENSE env var)\n\nOptional steps (can be configured later):\n- Graph configuration (only needed for Microsoft Graph/Copilot integration)\n- SSL/TLS settings\n- Proxy settings","operationId":"complete_setup_api_v1_setup_complete_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/setup/license":{"post":{"tags":["setup"],"summary":"Configure License","description":"Configure NetApp Connector license.\n\n**Required**: License key for the connector\n\nWhen in license reconfiguration mode, this endpoint will validate the new license\nand trigger an automatic restart if the license is valid.\n\n**Example**:\n```json\n{\n \"license_key\": \"your-license-key-here\"\n}\n```","operationId":"configure_license_api_v1_setup_license_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_configure_license_api_v1_setup_license_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/setup/license/status":{"get":{"tags":["setup"],"summary":"Get License Status","description":"Get current license status and connector ID.\n\nThis endpoint is especially useful when the application is in license\nreconfiguration mode due to a license mismatch or missing license.\n\nReturns:\n- connector_id: The ID used for license validation (provide this to licensing support)\n- license_configured: Whether a license key is present\n- license_valid: Whether the license is valid for this connector\n- error_message: Details about any license validation error\n- in_reconfiguration_mode: Whether the app is in license reconfiguration mode","operationId":"get_license_status_api_v1_setup_license_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LicenseStatusResponse"}}}}}}},"/api/v1/setup/graph":{"get":{"tags":["setup"],"summary":"Get Graph Settings","description":"Get current Microsoft Graph settings.\n\nReturns the current Graph configuration (client_secret is not returned for security).","operationId":"get_graph_settings_api_v1_setup_graph_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GraphSettingsResponse"}}}}}},"post":{"tags":["setup"],"summary":"Configure Graph","description":"Configure Microsoft Graph credentials for Copilot integration.\n\n**Note**: This step is OPTIONAL. Only configure if you want to integrate with\nMicrosoft Graph/Copilot. If you only want to index files to the local database,\nyou can skip this step.\n\n**Required** (if configuring Graph):\n- tenant_id: Azure AD Tenant ID\n- client_id: Application (client) ID\n- client_secret: Client secret value\n\n**Optional**:\n- connector_id: Custom connector ID (default: \"netappneo\")\n- connector_name: Display name for the connector\n- connector_description: Description of the connector\n\n**Example**:\n```json\n{\n \"tenant_id\": \"your-tenant-id\",\n \"client_id\": \"your-client-id\",\n \"client_secret\": \"your-client-secret\",\n \"connector_id\": \"netappneo\",\n \"connector_name\": \"NetApp NEO Connector\",\n \"connector_description\": \"SMB Share Integration\"\n}\n```","operationId":"configure_graph_api_v1_setup_graph_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_configure_graph_api_v1_setup_graph_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/setup/mcp":{"get":{"tags":["setup"],"summary":"Get Mcp Oauth Settings","description":"Get current MCP OAuth settings.\n\nReturns the current MCP OAuth configuration (client_secret is not returned for security).","operationId":"get_mcp_oauth_settings_api_v1_setup_mcp_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MCPOAuthSettingsResponse"}}}}}},"post":{"tags":["setup"],"summary":"Configure Mcp Oauth","description":"Configure MCP OAuth settings for Claude Desktop and MCP client authentication.\n\nThis configures OAuth 2.0 authentication for the MCP (Model Context Protocol) endpoint,\nallowing Claude Desktop and other MCP clients to authenticate using Microsoft Entra ID.\n\n**Required**:\n- tenant_id: Azure AD Tenant ID\n- client_id: Application (client) ID from Azure App Registration\n- client_secret: Client secret value\n\n**Optional**:\n- audience: Token audience for validation (defaults to client_id if not specified)\n\n**Example**:\n```json\n{\n \"tenant_id\": \"your-tenant-id\",\n \"client_id\": \"your-mcp-client-id\",\n \"client_secret\": \"your-mcp-client-secret\",\n \"audience\": \"api://your-mcp-client-id\"\n}\n```\n\n**Note**: The MCP OAuth configuration is separate from the Graph API configuration.\nMCP OAuth is used for authenticating MCP clients (like Claude Desktop),\nwhile Graph API credentials are used for Microsoft 365 Copilot integration.","operationId":"configure_mcp_oauth_api_v1_setup_mcp_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_configure_mcp_oauth_api_v1_setup_mcp_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/setup/mcp/api-key":{"get":{"tags":["setup"],"summary":"Get Mcp Api Key Status","description":"Check MCP API key configuration status.\n\nReturns whether an API key is configured (in DB and/or env var) and\nthe associated user metadata. **Never returns the actual key.**","operationId":"get_mcp_api_key_status_api_v1_setup_mcp_api_key_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MCPApiKeyStatusResponse"}}}}}},"post":{"tags":["setup"],"summary":"Create Mcp Api Key","description":"Create or regenerate an MCP API key.\n\nWhen set, clients can authenticate to the MCP endpoint by sending this\nvalue as a Bearer token instead of going through OAuth.\n\nThe generated key is returned **only once** in the response. Store it securely.\n\n**Example**:\n```json\n{\n \"email\": \"demo@example.com\",\n \"name\": \"Demo User\"\n}\n```","operationId":"create_mcp_api_key_api_v1_setup_mcp_api_key_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MCPApiKeyCreateRequest","default":{"email":"mcp-api-key@local","name":"MCP API Key User","sub":"mcp-api-key"}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["setup"],"summary":"Delete Mcp Api Key","description":"Revoke the MCP API key stored in the database.\n\nDeletes all `mcp.api_key*` config keys from the database.\nIf the `MCP_API_KEY` environment variable is also set, it will remain active\nuntil removed from the deployment configuration.","operationId":"delete_mcp_api_key_api_v1_setup_mcp_api_key_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/setup/ssl":{"get":{"tags":["setup"],"summary":"Get Ssl Settings","description":"Get current SSL/TLS settings.\n\nReturns the current SSL verification, timeout, and custom CA certificate status.","operationId":"get_ssl_settings_api_v1_setup_ssl_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SSLSettingsResponse"}}}}}},"post":{"tags":["setup"],"summary":"Configure Ssl","description":"Configure SSL/TLS settings for Microsoft Graph connections.\n\n**Optional**:\n- verify_ssl: Enable/disable SSL verification (default: true)\n- timeout: Request timeout in seconds (default: 30)\n- ca_certificate: Full PEM-encoded CA certificate content (including -----BEGIN CERTIFICATE-----)\n- allow_legacy_certificates: Allow CA certificates without keyUsage X.509 extension (default: false)\n\n**Example**:\n```json\n{\n \"verify_ssl\": true,\n \"timeout\": 30,\n \"ca_certificate\": \"-----BEGIN CERTIFICATE-----\\nMIIDXTCCAkWgAwIBAgIJAKJ...\\n-----END CERTIFICATE-----\",\n \"allow_legacy_certificates\": false\n}\n```\n\n**Note**: The certificate content will be stored in the database and combined with the default\ncertifi CA bundle. No file system access or volume mounts required.\n\n**Legacy Certificate Mode**: Some corporate proxy CA certificates don't include the keyUsage\nX.509 extension, which Python 3.13+ requires by default. Setting `allow_legacy_certificates: true`\ndisables this strict checking. Only enable this if you trust your proxy CA and cannot obtain\na properly configured certificate.","operationId":"configure_ssl_api_v1_setup_ssl_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_configure_ssl_api_v1_setup_ssl_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/setup/proxy":{"get":{"tags":["setup"],"summary":"Get Proxy Settings","description":"Get current proxy settings.\n\nReturns the current proxy configuration (password is not returned for security).","operationId":"get_proxy_settings_api_v1_setup_proxy_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProxySettingsResponse"}}}}}},"post":{"tags":["setup"],"summary":"Configure Proxy","description":"Configure proxy settings for Microsoft Graph connections.\n\n**Optional**:\n- proxy_url: Proxy server URL (e.g., \"http://proxy.example.com:8080\")\n- proxy_username: Proxy authentication username\n- proxy_password: Proxy authentication password\n\n**Example**:\n```json\n{\n \"proxy_url\": \"http://proxy.example.com:8080\",\n \"proxy_username\": \"user\",\n \"proxy_password\": \"pass\"\n}\n```\n\n**Note**: To remove proxy configuration, call with all fields set to null.","operationId":"configure_proxy_api_v1_setup_proxy_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_configure_proxy_api_v1_setup_proxy_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/setup/reload":{"post":{"tags":["setup"],"summary":"Reload Settings","description":"Hot-reload SSL/proxy settings without restarting the service.\n\nThis endpoint reloads SSL and proxy settings from the database and applies them\nto the OAuth provider, allowing settings changes to take effect immediately\nwithout requiring a service restart.\n\n**Use Cases**:\n- After configuring SSL settings via POST /api/v1/setup/ssl\n- After configuring proxy settings via POST /api/v1/setup/proxy\n- When testing different SSL/proxy configurations\n\n**Note**: This only affects the API service's OAuth provider (used for MCP authentication).\nThe worker service will need to be restarted separately to pick up new settings\nfor Microsoft Graph API calls.","operationId":"reload_settings_api_v1_setup_reload_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReloadSettingsResponse"}}}}}}},"/api/v1/setup/reset":{"post":{"tags":["setup"],"summary":"Reset Setup","description":"Reset setup state (for testing or reconfiguration).\n\n**Warning**: This will reset all setup progress. Use with caution.","operationId":"reset_setup_api_v1_setup_reset_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/setup/extractors":{"get":{"tags":["setup"],"summary":"List Extractor Nodes","description":"List all registered extractor nodes.\n\nProxies the request to the worker service's /admin/extractor-status endpoint\nwhich has the live health state.","operationId":"list_extractor_nodes_api_v1_setup_extractors_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"tags":["setup"],"summary":"Add Extractor Node","description":"Manually register an extractor node.\n\nInserts the node into the database with source='manual', then notifies the\nworker service to refresh its node list.","operationId":"add_extractor_node_api_v1_setup_extractors_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtractorNodeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["setup"],"summary":"Remove Extractor Node","description":"Remove a manually-added extractor node.\n\nDeletes the node from the database and notifies the worker service.\nOnly manually-added nodes can be removed via this endpoint.","operationId":"remove_extractor_node_api_v1_setup_extractors_delete","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtractorNodeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/setup/factory-reset":{"post":{"tags":["setup"],"summary":"Factory Reset","description":"Perform a complete factory reset of the connector.\n\n**DANGER: This is a destructive operation that cannot be undone!**\n\nThis will:\n- Delete ALL shares and their configurations\n- Delete ALL file metadata and inventory\n- Delete ALL work queue items\n- Delete ALL users (including admin)\n- Delete ALL operation logs\n- Reset ALL system configuration (except encryption key if preserve_encryption_key=true)\n- Create a new admin account with a new auto-generated password\n\n**Use Cases:**\n- Fresh start after testing\n- Decommissioning and recommissioning\n- Recovery from corrupted state\n\n**Requirements:**\n- Must set `confirm: true` in the request body","operationId":"factory_reset_api_v1_setup_factory_reset_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FactoryResetRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FactoryResetResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/setup/graph/connections":{"get":{"tags":["setup"],"summary":"Get Graph Connections","description":"Get Graph connector connections.","operationId":"get_graph_connections_api_v1_setup_graph_connections_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GraphConnectionsResponse"}}}}}}},"/api/v1/setup/graph/connections/{connection_id}":{"get":{"tags":["setup"],"summary":"Get Graph Connection","description":"Get specific Graph connection.","operationId":"get_graph_connection_api_v1_setup_graph_connections__connection_id__get","parameters":[{"name":"connection_id","in":"path","required":true,"schema":{"type":"string","title":"Connection Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GraphConnectionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/setup/initial-credentials":{"get":{"tags":["setup"],"summary":"Get Initial Credentials","description":"Retrieve the initial admin credentials.\n\n**Security Notice**: This endpoint is only available if:\n1. The admin user has never logged in (last_login is NULL)\n2. The initial password is still stored in the system\n\nAfter the admin user logs in for the first time, this endpoint will return 403 Forbidden\nand the stored password will be permanently deleted.\n\nThis allows administrators to retrieve the auto-generated password without\nneeding access to container logs.","operationId":"get_initial_credentials_api_v1_setup_initial_credentials_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InitialCredentialsResponse"}}}}}}},"/api/v1/tasks":{"get":{"tags":["Tasks"],"summary":"List Tasks","description":"List background tasks.\n\nNote: Background tasks are managed by the worker service. This endpoint\nqueries the background_tasks table for task status information.","operationId":"list_tasks_api_v1_tasks_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"task_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Task Type"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tasks/{task_id}":{"get":{"tags":["Tasks"],"summary":"Get Task","description":"Get a specific task by ID.","operationId":"get_task_api_v1_tasks__task_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"string","title":"Task Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Tasks"],"summary":"Cancel Task","description":"Cancel a running task.","operationId":"cancel_task_api_v1_tasks__task_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"string","title":"Task Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tasks/{task_id}/detailed":{"get":{"tags":["Tasks"],"summary":"Get Task Detailed","description":"Get detailed task information.","operationId":"get_task_detailed_api_v1_tasks__task_id__detailed_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"string","title":"Task Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tasks/statistics/summary":{"get":{"tags":["Tasks"],"summary":"Get Task Statistics","description":"Get task statistics summary.","operationId":"get_task_statistics_api_v1_tasks_statistics_summary_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/tasks/statistics/acl-cache":{"get":{"tags":["Tasks"],"summary":"Get Acl Cache Statistics","description":"Get ACL resolution cache statistics.\n\nProxies to the worker service which maintains the in-memory ACL cache.\n\nReturns:\n - Cache size and capacity\n - Hit/miss counts and hit rate\n - Eviction count\n - Performance metrics","operationId":"get_acl_cache_statistics_api_v1_tasks_statistics_acl_cache_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/.well-known/oauth-authorization-server":{"get":{"tags":["MCP"],"summary":"OAuth Authorization Server Metadata","description":"Returns OAuth 2.0 Authorization Server Metadata as per RFC8414","operationId":"get_authorization_server_metadata__well_known_oauth_authorization_server_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/openid-configuration":{"get":{"tags":["MCP"],"summary":"OpenID Configuration","description":"Returns OpenID Connect Discovery metadata (aliases OAuth Authorization Server Metadata)","operationId":"get_openid_configuration__well_known_openid_configuration_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/jwks.json":{"get":{"tags":["MCP"],"summary":"JSON Web Key Set","description":"Returns the public keys used to sign tokens","operationId":"get_jwks__well_known_jwks_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/userinfo":{"get":{"tags":["MCP"],"summary":"OpenID Connect UserInfo","description":"Returns claims about the authenticated user","operationId":"userinfo_userinfo_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/register":{"post":{"tags":["MCP"],"summary":"OAuth Dynamic Client Registration","description":"Registers a new OAuth client dynamically as per RFC 7591","operationId":"register_client_register_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/authorize":{"get":{"tags":["MCP"],"summary":"OAuth Authorization Endpoint","description":"Redirects to Microsoft Entra ID for authentication","operationId":"authorize_authorize_get","parameters":[{"name":"response_type","in":"query","required":true,"schema":{"type":"string","title":"Response Type"}},{"name":"client_id","in":"query","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"redirect_uri","in":"query","required":true,"schema":{"type":"string","title":"Redirect Uri"}},{"name":"scope","in":"query","required":false,"schema":{"type":"string","default":"openid profile email","title":"Scope"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"code_challenge","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code Challenge"}},{"name":"code_challenge_method","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code Challenge Method"}},{"name":"resource","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resource"}},{"name":"nonce","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nonce"}},{"name":"prompt","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prompt"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/callback":{"get":{"tags":["MCP"],"summary":"OAuth Callback Endpoint","description":"Receives authorization code from Entra and forwards to client","operationId":"oauth_callback_oauth_callback_get","parameters":[{"name":"code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"error","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},{"name":"error_description","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Description"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mcp/token":{"post":{"tags":["MCP"],"summary":"OAuth Token Endpoint (MCP path)","description":"Exchanges authorization code for tokens via Entra ID, or authenticates local users","operationId":"token_endpoint_mcp_token_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/mcp/.well-known/oauth-protected-resource":{"get":{"tags":["MCP"],"summary":"OAuth Protected Resource Metadata (MCP path)","description":"Returns OAuth 2.0 Protected Resource Metadata as per RFC9728","operationId":"get_protected_resource_metadata_mcp__well_known_oauth_protected_resource_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/oauth-protected-resource/mcp":{"get":{"tags":["MCP"],"summary":"OAuth Protected Resource Metadata (path-based lookup)","description":"Returns OAuth 2.0 Protected Resource Metadata as per RFC9728 for /mcp resource","operationId":"get_protected_resource_metadata__well_known_oauth_protected_resource_mcp_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/oauth-protected-resource":{"get":{"tags":["MCP"],"summary":"OAuth Protected Resource Metadata","description":"Returns OAuth 2.0 Protected Resource Metadata as per RFC9728","operationId":"get_protected_resource_metadata__well_known_oauth_protected_resource_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/mcp":{"post":{"tags":["MCP"],"summary":"MCP Streamable HTTP Endpoint","description":"Handle MCP JSON-RPC requests over HTTP","operationId":"mcp_post_mcp_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["MCP"],"summary":"MCP SSE Endpoint","description":"Server-Sent Events endpoint for server-to-client messages","operationId":"mcp_get_mcp_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"post":{"tags":["MCP"],"summary":"MCP Streamable HTTP Endpoint (root)","description":"Handle MCP JSON-RPC requests over HTTP at root path","operationId":"mcp_post__post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["MCP"],"summary":"MCP SSE Endpoint (root)","description":"Server-Sent Events endpoint at root path","operationId":"mcp_get__get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mcp/info":{"get":{"tags":["MCP"],"summary":"MCP Server Info","description":"Returns information about the MCP server","operationId":"mcp_info_mcp_info_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/mcp/openapi.yaml":{"get":{"tags":["MCP"],"summary":"MCP OpenAPI Schema","description":"Returns OpenAPI schema for Copilot Studio integration","operationId":"mcp_openapi_yaml_mcp_openapi_yaml_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/mcp/openapi.json":{"get":{"tags":["MCP"],"summary":"MCP OpenAPI Schema (JSON)","description":"Returns OpenAPI schema in JSON format for Copilot Studio integration","operationId":"mcp_openapi_json_mcp_openapi_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"AddItemsRequest":{"properties":{"file_ids":{"items":{"type":"string"},"type":"array","minItems":1,"title":"File Ids","description":"File IDs to add"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes","description":"Optional notes for the added items"}},"type":"object","required":["file_ids"],"title":"AddItemsRequest","description":"Add files to an existing dataset."},"AdminUserCreate":{"properties":{"username":{"type":"string","title":"Username"},"password":{"type":"string","title":"Password"},"is_admin":{"type":"boolean","title":"Is Admin","default":false}},"type":"object","required":["username","password"],"title":"AdminUserCreate","description":"User creation model for admin endpoint."},"AllSharesFileListResponse":{"properties":{"files":{"items":{"$ref":"#/components/schemas/FlexibleFileResponse"},"type":"array","title":"Files"},"total_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Count"},"total_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Size"},"page":{"type":"integer","title":"Page"},"page_size":{"type":"integer","title":"Page Size"},"total_pages":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Pages"},"has_next":{"type":"boolean","title":"Has Next"},"has_previous":{"type":"boolean","title":"Has Previous"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"content_truncated":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Content Truncated"},"truncated_file_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Truncated File Count"},"max_content_length_applied":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Content Length Applied"},"response_size_warning":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Response Size Warning"}},"type":"object","required":["files","page","page_size","has_next","has_previous"],"title":"AllSharesFileListResponse","description":"Response model for cross-share file listing with pagination.\n\n``total_count`` / ``total_size`` are optional — see ``FileListResponse``.\n``next_cursor`` is populated when keyset pagination is in use."},"Body_configure_graph_api_v1_setup_graph_post":{"properties":{"tenant_id":{"type":"string","title":"Tenant Id"},"client_id":{"type":"string","title":"Client Id"},"client_secret":{"type":"string","title":"Client Secret"},"connector_id":{"type":"string","title":"Connector Id","default":"netappneo"},"connector_name":{"type":"string","title":"Connector Name"},"connector_description":{"type":"string","title":"Connector Description"}},"type":"object","required":["tenant_id","client_id","client_secret"],"title":"Body_configure_graph_api_v1_setup_graph_post"},"Body_configure_license_api_v1_setup_license_post":{"properties":{"license_key":{"type":"string","title":"License Key"}},"type":"object","required":["license_key"],"title":"Body_configure_license_api_v1_setup_license_post"},"Body_configure_mcp_oauth_api_v1_setup_mcp_post":{"properties":{"tenant_id":{"type":"string","title":"Tenant Id","description":"Azure AD Tenant ID for MCP OAuth"},"client_id":{"type":"string","title":"Client Id","description":"Application (client) ID for MCP OAuth"},"client_secret":{"type":"string","title":"Client Secret","description":"Client secret for MCP OAuth"},"audience":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Audience","description":"Token audience (optional, defaults to client_id)"}},"type":"object","required":["tenant_id","client_id","client_secret"],"title":"Body_configure_mcp_oauth_api_v1_setup_mcp_post"},"Body_configure_proxy_api_v1_setup_proxy_post":{"properties":{"proxy_url":{"type":"string","title":"Proxy Url"},"proxy_username":{"type":"string","title":"Proxy Username"},"proxy_password":{"type":"string","title":"Proxy Password"}},"type":"object","title":"Body_configure_proxy_api_v1_setup_proxy_post"},"Body_configure_ssl_api_v1_setup_ssl_post":{"properties":{"verify_ssl":{"type":"boolean","title":"Verify Ssl","default":true},"timeout":{"type":"integer","title":"Timeout","default":30},"ca_certificate":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ca Certificate","description":"PEM-encoded CA certificate content"},"allow_legacy_certificates":{"type":"boolean","title":"Allow Legacy Certificates","description":"Allow CA certificates without keyUsage extension (less secure, for corporate proxies)","default":false}},"type":"object","title":"Body_configure_ssl_api_v1_setup_ssl_post"},"Body_login_for_access_token_token_post":{"properties":{"grant_type":{"anyOf":[{"type":"string","pattern":"^password$"},{"type":"null"}],"title":"Grant Type"},"username":{"type":"string","title":"Username"},"password":{"type":"string","format":"password","title":"Password"},"scope":{"type":"string","title":"Scope","default":""},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"},"client_secret":{"anyOf":[{"type":"string"},{"type":"null"}],"format":"password","title":"Client Secret"}},"type":"object","required":["username","password"],"title":"Body_login_for_access_token_token_post"},"Body_retry_failed_items_api_v1_monitoring_retry_failed_post":{"properties":{"work_item_ids":{"anyOf":[{"items":{},"type":"array"},{"type":"null"}],"title":"Work Item Ids"}},"type":"object","title":"Body_retry_failed_items_api_v1_monitoring_retry_failed_post"},"Body_retry_work_items_api_v1_monitoring_work_items_retry_post":{"properties":{"work_item_ids":{"anyOf":[{"items":{},"type":"array"},{"type":"null"}],"title":"Work Item Ids"}},"type":"object","title":"Body_retry_work_items_api_v1_monitoring_work_items_retry_post"},"CreateDatasetRequest":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"Dataset name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description","description":"Dataset description"},"file_ids":{"items":{"type":"string"},"type":"array","minItems":1,"title":"File Ids","description":"File IDs to include"},"source_query":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Source Query","description":"Original search query for reference"},"is_public":{"type":"boolean","title":"Is Public","description":"Whether the dataset is publicly visible","default":false},"acl_override_enabled":{"type":"boolean","title":"Acl Override Enabled","description":"Bypass file-level ACLs for shared users","default":false},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At","description":"Expiration date (defaults to 90 days)"}},"type":"object","required":["name","file_ids"],"title":"CreateDatasetRequest","description":"Create a virtual dataset from search results."},"CreateSubsetRequest":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"New dataset name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description","description":"New dataset description"},"file_ids":{"items":{"type":"string"},"type":"array","minItems":1,"title":"File Ids","description":"File IDs (must be in source dataset)"},"acl_override_enabled":{"type":"boolean","title":"Acl Override Enabled","description":"Bypass file-level ACLs for shared users","default":false},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At","description":"Expiration date (defaults to 90 days)"}},"type":"object","required":["name","file_ids"],"title":"CreateSubsetRequest","description":"Create a new dataset from a subset of an existing dataset's items."},"DatasetExpirationResponse":{"properties":{"datasets":{"items":{"$ref":"#/components/schemas/DatasetResponse"},"type":"array","title":"Datasets"},"total_expiring":{"type":"integer","title":"Total Expiring"}},"type":"object","required":["datasets","total_expiring"],"title":"DatasetExpirationResponse","description":"Datasets nearing expiration."},"DatasetItemResponse":{"properties":{"id":{"type":"string","title":"Id"},"file_id":{"type":"string","title":"File Id"},"filename":{"type":"string","title":"Filename"},"file_path":{"type":"string","title":"File Path"},"unc_path":{"type":"string","title":"Unc Path"},"share_id":{"type":"string","title":"Share Id"},"share_name":{"type":"string","title":"Share Name"},"size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Size"},"modified_time":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Modified Time"},"file_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"File Type"},"added_at":{"type":"string","format":"date-time","title":"Added At"},"added_by_username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Added By Username"},"position":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Position"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","required":["id","file_id","filename","file_path","unc_path","share_id","share_name","added_at"],"title":"DatasetItemResponse","description":"File item within a dataset."},"DatasetItemsResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/DatasetItemResponse"},"type":"array","title":"Items"},"total_count":{"type":"integer","title":"Total Count"},"page":{"type":"integer","title":"Page"},"page_size":{"type":"integer","title":"Page Size"},"total_pages":{"type":"integer","title":"Total Pages"},"has_next":{"type":"boolean","title":"Has Next"},"has_previous":{"type":"boolean","title":"Has Previous"}},"type":"object","required":["items","total_count","page","page_size","total_pages","has_next","has_previous"],"title":"DatasetItemsResponse","description":"Paginated list of dataset items."},"DatasetListResponse":{"properties":{"datasets":{"items":{"$ref":"#/components/schemas/DatasetResponse"},"type":"array","title":"Datasets"},"total_count":{"type":"integer","title":"Total Count"},"page":{"type":"integer","title":"Page"},"page_size":{"type":"integer","title":"Page Size"},"total_pages":{"type":"integer","title":"Total Pages"},"has_next":{"type":"boolean","title":"Has Next"},"has_previous":{"type":"boolean","title":"Has Previous"}},"type":"object","required":["datasets","total_count","page","page_size","total_pages","has_next","has_previous"],"title":"DatasetListResponse","description":"Paginated list of datasets."},"DatasetPermission":{"type":"string","enum":["read","write","admin"],"title":"DatasetPermission","description":"Permission levels for virtual dataset access."},"DatasetResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"owner_id":{"type":"integer","title":"Owner Id"},"owner_username":{"type":"string","title":"Owner Username"},"is_public":{"type":"boolean","title":"Is Public"},"acl_override_enabled":{"type":"boolean","title":"Acl Override Enabled"},"item_count":{"type":"integer","title":"Item Count"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"expires_in_hours":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Expires In Hours","description":"Hours until expiration"},"source_query":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Source Query"},"user_permission":{"type":"string","title":"User Permission","description":"Current user's effective permission"},"metadata":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Metadata"}},"type":"object","required":["id","name","owner_id","owner_username","is_public","acl_override_enabled","item_count","created_at","updated_at","user_permission"],"title":"DatasetResponse","description":"Virtual dataset details."},"DatasetSearchRequest":{"properties":{"query":{"type":"string","maxLength":500,"minLength":1,"title":"Query","description":"Search query"},"file_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"File Types","description":"Filter by file types"},"page":{"type":"integer","minimum":1.0,"title":"Page","description":"Page number","default":1},"page_size":{"type":"integer","maximum":1000.0,"minimum":1.0,"title":"Page Size","description":"Results per page","default":100},"sort_by":{"type":"string","title":"Sort By","description":"Sort by: relevance, modified_time, filename, size","default":"relevance"},"sort_order":{"type":"string","title":"Sort Order","description":"Sort order: asc or desc","default":"desc"}},"type":"object","required":["query"],"title":"DatasetSearchRequest","description":"Search within a dataset using full-text search."},"DatasetSearchResponse":{"properties":{"results":{"items":{"$ref":"#/components/schemas/SearchResultItem"},"type":"array","title":"Results"},"total_count":{"type":"integer","title":"Total Count"},"page":{"type":"integer","title":"Page"},"page_size":{"type":"integer","title":"Page Size"},"total_pages":{"type":"integer","title":"Total Pages"},"has_next":{"type":"boolean","title":"Has Next"},"has_previous":{"type":"boolean","title":"Has Previous"},"query":{"type":"string","title":"Query"},"search_time_ms":{"type":"integer","title":"Search Time Ms"}},"type":"object","required":["results","total_count","page","page_size","total_pages","has_next","has_previous","query","search_time_ms"],"title":"DatasetSearchResponse","description":"Search results scoped to a dataset."},"DatasetShareResponse":{"properties":{"id":{"type":"string","title":"Id"},"user_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"User Id"},"username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Username"},"entra_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entra User Id"},"entra_group_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entra Group Id"},"permission":{"$ref":"#/components/schemas/DatasetPermission"},"shared_by_id":{"type":"integer","title":"Shared By Id"},"shared_by_username":{"type":"string","title":"Shared By Username"},"shared_at":{"type":"string","format":"date-time","title":"Shared At"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"}},"type":"object","required":["id","permission","shared_by_id","shared_by_username","shared_at"],"title":"DatasetShareResponse","description":"Share permission on a dataset."},"EnumerationStatsResponse":{"properties":{"active_enumerations":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Active Enumerations"},"enumeration_queue_depth":{"additionalProperties":{"type":"integer"},"type":"object","title":"Enumeration Queue Depth"},"completed_enumerations_last_24h":{"type":"integer","title":"Completed Enumerations Last 24H"},"avg_enumeration_duration_seconds":{"type":"number","title":"Avg Enumeration Duration Seconds"}},"type":"object","required":["active_enumerations","enumeration_queue_depth","completed_enumerations_last_24h","avg_enumeration_duration_seconds"],"title":"EnumerationStatsResponse","description":"Response model for enumeration statistics"},"ExtractorNodeRequest":{"properties":{"url":{"type":"string","title":"Url","description":"Base URL of the extractor service (e.g. http://192.168.1.50:8000)"}},"type":"object","required":["url"],"title":"ExtractorNodeRequest","description":"Request model for adding/removing extractor nodes."},"FactoryResetRequest":{"properties":{"confirm":{"type":"boolean","title":"Confirm","description":"Must be set to true to confirm factory reset"},"preserve_encryption_key":{"type":"boolean","title":"Preserve Encryption Key","description":"If true, preserves the encryption key (recommended for data recovery scenarios)","default":true}},"type":"object","required":["confirm"],"title":"FactoryResetRequest","description":"Request model for factory reset."},"FactoryResetResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"type":"string","title":"Message"},"tables_cleared":{"items":{},"type":"array","title":"Tables Cleared"},"warnings":{"items":{},"type":"array","title":"Warnings","default":[]},"next_steps":{"items":{},"type":"array","title":"Next Steps","default":[]}},"type":"object","required":["success","message","tables_cleared"],"title":"FactoryResetResponse","description":"Response model for factory reset."},"FileListResponse":{"properties":{"share_id":{"type":"string","title":"Share Id"},"path":{"type":"string","title":"Path"},"files":{"items":{"$ref":"#/components/schemas/FlexibleFileResponse"},"type":"array","title":"Files"},"total_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Count"},"total_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Size"},"page":{"type":"integer","title":"Page"},"page_size":{"type":"integer","title":"Page Size"},"total_pages":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Pages"},"has_next":{"type":"boolean","title":"Has Next"},"has_previous":{"type":"boolean","title":"Has Previous"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"content_truncated":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Content Truncated"},"truncated_file_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Truncated File Count"},"max_content_length_applied":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Content Length Applied"},"response_size_warning":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Response Size Warning"}},"type":"object","required":["share_id","path","files","page","page_size","has_next","has_previous"],"title":"FileListResponse","description":"Response model for file listing with pagination.\n\n``total_count`` and ``total_size`` are optional: callers can suppress them\nwith ``?include_counts=false`` to skip the cross-partition aggregates that\ndominate request latency at billion-row scale. ``total_pages``, ``has_next``,\nand ``has_previous`` are derived from page-vs-returned-rows when totals are\nomitted, so basic pagination still works without them.\n\n``next_cursor`` is set when the request used keyset pagination\n(``?after_modified_time=...``); pass it back as the next call's cursor to\nwalk through millions of rows in O(log N) per page rather than O(N) per\ndeep-offset page."},"FlexibleFileResponse":{"properties":{"id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Id"},"file_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"File Path"},"unc_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Unc Path"},"filename":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Filename"},"size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Size"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"modified_time":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Modified Time"},"accessed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Accessed At"},"is_directory":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Directory"},"file_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"File Type"},"content":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content"},"content_chunks":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Content Chunks"},"conversion_duration_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Conversion Duration Ms"},"extractor_used":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Extractor Used"},"indexed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Indexed At"},"acl_principals":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Acl Principals"},"resolved_principals":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Resolved Principals"},"share_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Share Id"},"share_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Share Name"},"share_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Share Path"}},"additionalProperties":true,"type":"object","title":"FlexibleFileResponse","description":"Flexible file response supporting dynamic field selection.\n\nOnly fields that are explicitly set will be serialized to JSON.\nThis prevents null values for unrequested fields from cluttering the response."},"FullTextSearchRequest":{"properties":{"query":{"type":"string","maxLength":500,"minLength":1,"title":"Query","description":"Search query (natural language)"},"share_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Share Ids","description":"Filter by specific share IDs"},"file_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"File Types","description":"Filter by file types (e.g., pdf, docx, txt)"},"modified_after":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Modified After","description":"Filter files modified after this date"},"modified_before":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Modified Before","description":"Filter files modified before this date"},"page":{"type":"integer","minimum":1.0,"title":"Page","description":"Page number (starting from 1)","default":1},"page_size":{"type":"integer","maximum":1000.0,"minimum":1.0,"title":"Page Size","description":"Results per page (max 1000)","default":100},"sort_by":{"type":"string","title":"Sort By","description":"Sort by: relevance, modified_time, filename, size","default":"relevance"},"sort_order":{"type":"string","title":"Sort Order","description":"Sort order: asc or desc","default":"desc"},"search_mode":{"type":"string","title":"Search Mode","description":"MySQL only: 'natural' (OR-based) or 'boolean' (AND-based, advanced)","default":"natural"}},"type":"object","required":["query"],"title":"FullTextSearchRequest","description":"Request model for full-text search"},"FullTextSearchResponse":{"properties":{"results":{"items":{"$ref":"#/components/schemas/SearchResultItem"},"type":"array","title":"Results"},"total_count":{"type":"integer","title":"Total Count"},"page":{"type":"integer","title":"Page"},"page_size":{"type":"integer","title":"Page Size"},"total_pages":{"type":"integer","title":"Total Pages"},"has_next":{"type":"boolean","title":"Has Next"},"has_previous":{"type":"boolean","title":"Has Previous"},"query":{"type":"string","title":"Query","description":"Original search query"},"search_time_ms":{"type":"integer","title":"Search Time Ms","description":"Search execution time in milliseconds"},"database_type":{"type":"string","title":"Database Type","description":"Database backend used (postgresql or mysql)"}},"type":"object","required":["results","total_count","page","page_size","total_pages","has_next","has_previous","query","search_time_ms","database_type"],"title":"FullTextSearchResponse","description":"Response model for full-text search with pagination and metadata"},"GraphConnectionResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tenant Id"},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"},"status":{"type":"string","title":"Status"}},"type":"object","required":["id","status"],"title":"GraphConnectionResponse","description":"Response model for a Graph connection."},"GraphConnectionsResponse":{"properties":{"connections":{"items":{"$ref":"#/components/schemas/GraphConnectionResponse"},"type":"array","title":"Connections"},"count":{"type":"integer","title":"Count"}},"type":"object","required":["connections","count"],"title":"GraphConnectionsResponse","description":"Response model for listing Graph connections."},"GraphRateLimitStatsResponse":{"properties":{"requests_made":{"type":"integer","title":"Requests Made"},"requests_remaining":{"type":"integer","title":"Requests Remaining"},"reset_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reset Time"},"rate_limited":{"type":"boolean","title":"Rate Limited"},"backoff_until":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Backoff Until"}},"type":"object","required":["requests_made","requests_remaining","reset_time","rate_limited","backoff_until"],"title":"GraphRateLimitStatsResponse","description":"Response model for Graph API rate limit statistics"},"GraphSettingsResponse":{"properties":{"graph_configured":{"type":"boolean","title":"Graph Configured","default":false},"tenant_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tenant Id"},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"},"client_secret_set":{"type":"boolean","title":"Client Secret Set","default":false},"connector_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Connector Id"},"connector_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Connector Name"},"connector_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Connector Description"},"message":{"type":"string","title":"Message","default":""}},"type":"object","title":"GraphSettingsResponse","description":"Graph settings response model."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"InitialCredentialsResponse":{"properties":{"username":{"type":"string","title":"Username"},"password":{"type":"string","title":"Password"},"message":{"type":"string","title":"Message"}},"type":"object","required":["username","password","message"],"title":"InitialCredentialsResponse","description":"Response model for initial admin credentials."},"LicenseDetails":{"properties":{"days_remaining":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Days Remaining"},"expiry_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Expiry Date"}},"type":"object","title":"LicenseDetails","description":"License detail fields (nested under 'details' for UI compatibility)."},"LicenseStatusResponse":{"properties":{"license_configured":{"type":"boolean","title":"License Configured"},"license_valid":{"type":"boolean","title":"License Valid","default":false},"connector_id":{"type":"string","title":"Connector Id"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"},"in_reconfiguration_mode":{"type":"boolean","title":"In Reconfiguration Mode","default":false},"message":{"type":"string","title":"Message"},"days_remaining":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Days Remaining"},"expiry_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Expiry Date"},"details":{"anyOf":[{"$ref":"#/components/schemas/LicenseDetails"},{"type":"null"}]}},"type":"object","required":["license_configured","connector_id","message"],"title":"LicenseStatusResponse","description":"License status response model."},"MCPApiKeyCreateRequest":{"properties":{"api_key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key","description":"Custom API key. If empty, a secure key is auto-generated."},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email","description":"Email address associated with the API key user","default":"mcp-api-key@local"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name","description":"Display name associated with the API key user","default":"MCP API Key User"},"sub":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sub","description":"Subject identifier for the API key user","default":"mcp-api-key"}},"type":"object","title":"MCPApiKeyCreateRequest","description":"Request model for creating/regenerating an MCP API key."},"MCPApiKeyStatusResponse":{"properties":{"api_key_set":{"type":"boolean","title":"Api Key Set","default":false},"env_var_set":{"type":"boolean","title":"Env Var Set","default":false},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"sub":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sub"},"message":{"type":"string","title":"Message","default":""}},"type":"object","title":"MCPApiKeyStatusResponse","description":"Response model for MCP API key status."},"MCPOAuthSettingsResponse":{"properties":{"mcp_oauth_configured":{"type":"boolean","title":"Mcp Oauth Configured","default":false},"tenant_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tenant Id"},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"},"client_secret_set":{"type":"boolean","title":"Client Secret Set","default":false},"audience":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Audience"},"message":{"type":"string","title":"Message","default":""}},"type":"object","title":"MCPOAuthSettingsResponse","description":"MCP OAuth settings response model."},"MonitoringOverviewResponse":{"properties":{"work_queue":{"$ref":"#/components/schemas/WorkQueueStatsResponse"},"enumeration":{"$ref":"#/components/schemas/EnumerationStatsResponse"},"workers":{"$ref":"#/components/schemas/WorkerStatsResponse"},"graph_rate_limit":{"$ref":"#/components/schemas/GraphRateLimitStatsResponse"},"timestamp":{"type":"string","format":"date-time","title":"Timestamp"}},"type":"object","required":["work_queue","enumeration","workers","graph_rate_limit"],"title":"MonitoringOverviewResponse","description":"Response model for monitoring overview"},"NERResultResponse":{"properties":{"id":{"type":"string","title":"Id"},"file_id":{"type":"string","title":"File Id"},"share_id":{"type":"string","title":"Share Id"},"filename":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Filename"},"file_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"File Path"},"entities":{"anyOf":[{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object"},{"type":"null"}],"title":"Entities"},"classifications":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Classifications"},"structured_data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Structured Data"},"schema_name":{"type":"string","title":"Schema Name"},"schema_version":{"type":"string","title":"Schema Version"},"confidence_threshold":{"type":"number","title":"Confidence Threshold"},"processing_time_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Processing Time Ms"},"analyzed_at":{"type":"string","format":"date-time","title":"Analyzed At"}},"type":"object","required":["id","file_id","share_id","schema_name","schema_version","confidence_threshold","analyzed_at"],"title":"NERResultResponse","description":"API response model for NER results"},"NERSchemaConfig":{"properties":{"name":{"type":"string","title":"Name"},"version":{"type":"string","title":"Version","default":"1.0"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"entity_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Entity Types"},"entity_descriptions":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Entity Descriptions"},"classifications":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Classifications"},"structured_extraction":{"anyOf":[{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object"},{"type":"null"}],"title":"Structured Extraction"},"confidence_threshold":{"type":"number","title":"Confidence Threshold","default":0.7},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["name"],"title":"NERSchemaConfig","description":"Configuration for a custom NER schema"},"NERSchemaListResponse":{"properties":{"schemas":{"items":{"$ref":"#/components/schemas/NERSchemaConfig"},"type":"array","title":"Schemas"},"total_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Count"},"count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Count"}},"type":"object","required":["schemas"],"title":"NERSchemaListResponse","description":"Response model for listing available NER schemas"},"NERSettingsUpdate":{"properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Enabled"},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model"},"batch_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Batch Size"},"confidence_threshold":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Confidence Threshold"},"device":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Device"}},"type":"object","title":"NERSettingsUpdate"},"NERStatsResponse":{"properties":{"total_entities":{"type":"integer","title":"Total Entities","default":0},"total_files_processed":{"type":"integer","title":"Total Files Processed","default":0},"entity_types":{"additionalProperties":{"type":"integer"},"type":"object","title":"Entity Types","default":{}},"processing_enabled":{"type":"boolean","title":"Processing Enabled","default":true},"total_files_analyzed":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Files Analyzed"},"files_pending_analysis":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Files Pending Analysis"},"files_failed_analysis":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Files Failed Analysis"},"avg_processing_time_ms":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Processing Time Ms"},"entity_counts":{"anyOf":[{"additionalProperties":{"type":"integer"},"type":"object"},{"type":"null"}],"title":"Entity Counts"},"classification_distribution":{"anyOf":[{"additionalProperties":{"additionalProperties":{"type":"integer"},"type":"object"},"type":"object"},{"type":"null"}],"title":"Classification Distribution"},"last_analysis_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Analysis At"}},"type":"object","title":"NERStatsResponse","description":"Response model for NER processing statistics"},"OperationLog":{"properties":{"id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Id"},"operation_type":{"type":"string","title":"Operation Type"},"status":{"type":"string","title":"Status"},"details":{"type":"string","title":"Details"},"timestamp":{"type":"string","format":"date-time","title":"Timestamp","default":"2026-05-11T12:18:21.396057"},"metadata":{"additionalProperties":true,"type":"object","title":"Metadata","default":{}},"user_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"User Id"},"username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Username"}},"type":"object","required":["operation_type","status","details"],"title":"OperationLog","description":"Operation log entry"},"PasswordChange":{"properties":{"current_password":{"type":"string","title":"Current Password"},"new_password":{"type":"string","title":"New Password"}},"type":"object","required":["current_password","new_password"],"title":"PasswordChange","description":"Request model for changing password."},"ProxySettingsResponse":{"properties":{"proxy_configured":{"type":"boolean","title":"Proxy Configured","default":false},"proxy_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proxy Url"},"proxy_username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proxy Username"},"proxy_password_set":{"type":"boolean","title":"Proxy Password Set","default":false},"message":{"type":"string","title":"Message","default":""}},"type":"object","title":"ProxySettingsResponse","description":"Proxy settings response model."},"ReloadSettingsResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"oauth_reloaded":{"type":"boolean","title":"Oauth Reloaded","default":false},"ssl_settings":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Ssl Settings"},"proxy_settings":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Proxy Settings"},"message":{"type":"string","title":"Message","default":""}},"type":"object","required":["success"],"title":"ReloadSettingsResponse","description":"Response model for settings reload."},"RemoveItemsRequest":{"properties":{"file_ids":{"items":{"type":"string"},"type":"array","minItems":1,"title":"File Ids","description":"File IDs to remove"}},"type":"object","required":["file_ids"],"title":"RemoveItemsRequest","description":"Remove files from a dataset."},"SSLSettingsResponse":{"properties":{"verify_ssl":{"type":"boolean","title":"Verify Ssl","default":true},"timeout":{"type":"integer","title":"Timeout","default":30},"custom_ca_certificate_configured":{"type":"boolean","title":"Custom Ca Certificate Configured","default":false},"allow_legacy_certificates":{"type":"boolean","title":"Allow Legacy Certificates","default":false},"message":{"type":"string","title":"Message","default":""}},"type":"object","title":"SSLSettingsResponse","description":"SSL settings response model."},"SearchResultItem":{"properties":{"id":{"type":"string","title":"Id"},"share_id":{"type":"string","title":"Share Id"},"filename":{"type":"string","title":"Filename"},"file_path":{"type":"string","title":"File Path"},"unc_path":{"type":"string","title":"Unc Path"},"size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Size"},"modified_time":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Modified Time"},"file_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"File Type"},"indexed_at":{"type":"string","format":"date-time","title":"Indexed At"},"relevance_score":{"type":"number","title":"Relevance Score","description":"Relevance score from full-text search engine"},"snippet":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Snippet","description":"Content preview with search context"},"share_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Share Name","description":"Share name for display"},"resolved_principals":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Resolved Principals","description":"Resolved ACL principals for access control"}},"type":"object","required":["id","share_id","filename","file_path","unc_path","size","modified_time","file_type","indexed_at","relevance_score"],"title":"SearchResultItem","description":"Individual search result item with relevance scoring"},"SetupStatusResponse":{"properties":{"setup_complete":{"type":"boolean","title":"Setup Complete"},"database_configured":{"type":"boolean","title":"Database Configured"},"database_url_environment_set":{"type":"boolean","title":"Database Url Environment Set","default":false},"config_storage":{"type":"string","title":"Config Storage","default":"file"},"steps_completed":{"items":{},"type":"array","title":"Steps Completed"},"required_steps":{"items":{},"type":"array","title":"Required Steps"},"optional_steps":{"items":{},"type":"array","title":"Optional Steps"},"message":{"type":"string","title":"Message","default":""},"persistence_info":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Persistence Info"},"license_reconfiguration_mode":{"type":"boolean","title":"License Reconfiguration Mode","default":false},"connector_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Connector Id"}},"type":"object","required":["setup_complete","database_configured","steps_completed","required_steps","optional_steps"],"title":"SetupStatusResponse","description":"Setup status response model."},"ShareConfig":{"properties":{"id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Id"},"protocol":{"$ref":"#/components/schemas/ShareProtocol","default":"smb"},"share_path":{"type":"string","title":"Share Path"},"username":{"type":"string","title":"Username","default":""},"password":{"type":"string","title":"Password","default":""},"created_at":{"type":"string","format":"date-time","title":"Created At","default":"2026-05-11T12:18:21.386475"},"last_crawled":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Crawled"},"last_crawl_duration_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Last Crawl Duration Ms"},"last_crawl_file_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Last Crawl File Count"},"crawl_schedule":{"type":"string","title":"Crawl Schedule","default":"0 0 * * *"},"rules":{"additionalProperties":true,"type":"object","title":"Rules"},"status":{"$ref":"#/components/schemas/ShareStatus","default":"initializing"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"},"last_connection_attempt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Connection Attempt"},"realm":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Realm"},"use_kerberos":{"type":"string","title":"Use Kerberos","default":"required"},"workgroup":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Workgroup"},"resolve_order":{"type":"string","title":"Resolve Order","default":"host"},"nfs_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nfs Version"},"nfs_security":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nfs Security"},"nfs_mount_options":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nfs Mount Options"},"smb_mount_options":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Smb Mount Options"},"s3_endpoint_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"S3 Endpoint Url"},"s3_region":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"S3 Region"},"s3_bucket":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"S3 Bucket"},"s3_prefix":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"S3 Prefix"},"s3_use_ssl":{"type":"boolean","title":"S3 Use Ssl","default":true}},"type":"object","required":["share_path"],"title":"ShareConfig","description":"Configuration for a share"},"ShareDatasetRequest":{"properties":{"user_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"User Id","description":"Local user ID"},"username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Username","description":"Local username (resolved to user_id)"},"entra_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entra User Id","description":"Entra user object ID"},"entra_user_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entra User Email","description":"Entra user email (resolved to object ID)"},"entra_group_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entra Group Id","description":"Entra group object ID"},"entra_group_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entra Group Name","description":"Entra group name (resolved to object ID)"},"permission":{"$ref":"#/components/schemas/DatasetPermission","description":"Permission level to grant","default":"read"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At","description":"Share expiration date"}},"type":"object","title":"ShareDatasetRequest","description":"Share a dataset with a user or group.\n\nExactly one target must be specified: user_id, username,\nentra_user_id, entra_user_email, entra_group_id, or entra_group_name."},"ShareProtocol":{"type":"string","enum":["smb","nfs","s3"],"title":"ShareProtocol","description":"Protocol type for data source connections"},"ShareResponse":{"properties":{"id":{"type":"string","title":"Id"},"protocol":{"$ref":"#/components/schemas/ShareProtocol","default":"smb"},"share_path":{"type":"string","title":"Share Path"},"username":{"type":"string","title":"Username"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"last_crawled":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Crawled"},"last_crawl_duration_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Last Crawl Duration Ms"},"last_crawl_file_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Last Crawl File Count"},"crawl_schedule":{"type":"string","title":"Crawl Schedule"},"rules":{"additionalProperties":true,"type":"object","title":"Rules"},"status":{"$ref":"#/components/schemas/ShareStatus"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"},"last_connection_attempt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Connection Attempt"},"realm":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Realm"},"use_kerberos":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Use Kerberos"},"workgroup":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Workgroup"},"resolve_order":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolve Order"},"nfs_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nfs Version"},"nfs_security":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nfs Security"},"nfs_mount_options":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nfs Mount Options"},"smb_mount_options":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Smb Mount Options"},"s3_endpoint_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"S3 Endpoint Url"},"s3_region":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"S3 Region"},"s3_bucket":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"S3 Bucket"},"s3_prefix":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"S3 Prefix"},"s3_use_ssl":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"S3 Use Ssl"}},"type":"object","required":["id","share_path","username","created_at","last_crawled","last_crawl_duration_ms","last_crawl_file_count","crawl_schedule","rules","status","error_message","last_connection_attempt"],"title":"ShareResponse","description":"Response model for share (excludes password)"},"ShareStatus":{"type":"string","enum":["initializing","connecting","connected","connection_failed","crawling","processing","ready","error"],"title":"ShareStatus","description":"Status of a share"},"ShareUpdate":{"properties":{"share_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Share Path"},"username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Username"},"password":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Password"},"crawl_schedule":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Crawl Schedule"},"rules":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Rules"},"realm":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Realm"},"use_kerberos":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Use Kerberos"},"workgroup":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Workgroup"},"resolve_order":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolve Order"},"nfs_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nfs Version"},"nfs_security":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nfs Security"},"nfs_mount_options":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nfs Mount Options"},"smb_mount_options":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Smb Mount Options"},"s3_endpoint_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"S3 Endpoint Url"},"s3_region":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"S3 Region"},"s3_bucket":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"S3 Bucket"},"s3_prefix":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"S3 Prefix"},"s3_use_ssl":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"S3 Use Ssl"}},"additionalProperties":false,"type":"object","title":"ShareUpdate","description":"Model for updating share configuration"},"Token":{"properties":{"access_token":{"type":"string","title":"Access Token"},"token_type":{"type":"string","title":"Token Type"}},"type":"object","required":["access_token","token_type"],"title":"Token","description":"Token response model."},"UpdateDatasetRequest":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"is_public":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Public"},"acl_override_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Acl Override Enabled"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"}},"type":"object","title":"UpdateDatasetRequest","description":"Update dataset metadata."},"UserCreate":{"properties":{"username":{"type":"string","title":"Username"},"password":{"type":"string","title":"Password"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"is_admin":{"type":"boolean","title":"Is Admin","default":false}},"type":"object","required":["username","password"],"title":"UserCreate","description":"Request model for creating a user."},"UserResponse":{"properties":{"id":{"type":"integer","title":"Id"},"username":{"type":"string","title":"Username"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"is_active":{"type":"boolean","title":"Is Active"},"is_admin":{"type":"boolean","title":"Is Admin"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"last_login":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Login"},"entra_object_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entra Object Id"},"entra_tenant_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entra Tenant Id"},"entra_display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entra Display Name"},"entra_linked_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Entra Linked At"}},"type":"object","required":["id","username","is_active","is_admin","created_at","last_login"],"title":"UserResponse","description":"Response model for user (excludes password)"},"UserUpdate":{"properties":{"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"password":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Password"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"},"is_admin":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Admin"}},"type":"object","title":"UserUpdate","description":"Request model for updating a user."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WorkQueueStatsResponse":{"properties":{"total_items":{"type":"integer","title":"Total Items"},"pending_items":{"type":"integer","title":"Pending Items"},"claimed_items":{"type":"integer","title":"Claimed Items"},"processing_items":{"type":"integer","title":"Processing Items"},"completed_items":{"type":"integer","title":"Completed Items"},"failed_items":{"type":"integer","title":"Failed Items"},"abandoned_items":{"type":"integer","title":"Abandoned Items"}},"type":"object","required":["total_items","pending_items","claimed_items","processing_items","completed_items","failed_items","abandoned_items"],"title":"WorkQueueStatsResponse","description":"Response model for work queue statistics"},"WorkerStatsResponse":{"properties":{"total_workers":{"type":"integer","title":"Total Workers"},"active_workers":{"type":"integer","title":"Active Workers"},"stopping_workers":{"type":"integer","title":"Stopping Workers"},"stopped_workers":{"type":"integer","title":"Stopped Workers"},"workers":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Workers"}},"type":"object","required":["total_workers","active_workers","stopping_workers","stopped_workers","workers"],"title":"WorkerStatsResponse","description":"Response model for worker statistics"}},"securitySchemes":{"OAuth2PasswordBearer":{"type":"oauth2","flows":{"password":{"tokenUrl":"token","scopes":{}}},"description":"Login with username and password."},"JWT":{"type":"apiKey","in":"header","name":"Authorization","description":"JWT Token in Authorization header. Example: Bearer "}}},"security":[{"OAuth2PasswordBearer":[]},{"JWT":[]}]} \ No newline at end of file diff --git a/nginx.conf b/nginx.conf deleted file mode 100644 index ae8b0f2..0000000 --- a/nginx.conf +++ /dev/null @@ -1,42 +0,0 @@ -server { - listen 80; - server_name localhost; - root /usr/share/nginx/html; - index index.html; - - # Serve the UI - location / { - try_files $uri $uri/ /index.html; - } - - # Proxy API requests to backend - # The NEO_API will be replaced at container startup - location /api/ { - rewrite ^/api/(.*) /$1 break; - proxy_pass ${NEO_API}; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # Don't buffer to see real-time responses - proxy_buffering off; - } - - # Cache static assets - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - } - - # Security headers - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - - # Gzip compression - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e6d2aa9..b5e1467 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "neo-ui-framework", - "version": "3.2.0", + "version": "4.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "neo-ui-framework", - "version": "3.2.0", + "version": "4.0.0", "dependencies": { "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", @@ -37,6 +37,8 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "cookie": "^1.0.2", + "i18next": "^26.0.8", + "i18next-icu": "^2.4.3", "js-yaml": "^4.1.1", "lucide-react": "^0.548.0", "next-themes": "^0.4.6", @@ -44,6 +46,7 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-hook-form": "^7.65.0", + "react-i18next": "^17.0.6", "react-markdown": "^10.1.0", "react-router-dom": "^7.9.4", "recharts": "^2.15.4", @@ -105,7 +108,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -310,9 +312,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -371,6 +373,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.0.0" }, @@ -1045,6 +1048,30 @@ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, + "node_modules/@formatjs/fast-memoize": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.2.tgz", + "integrity": "sha512-vPnriihkfK0lzoQGaXq+qXH23VsYyansRTkTgo2aTG0k1NjLFyZimFVdfj4C9JkSE5dm7CEngcQ5TTc1yAyBfQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.5.tgz", + "integrity": "sha512-ASMon8BNlKHgQQpZx84xI80EXRS90GlsEU4wEulCKCzrMtUdrfEvFc9UEYmRbvEvtFQLZ4qHXnisUy6PuFjwyA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@formatjs/icu-skeleton-parser": "2.1.5" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.5.tgz", + "integrity": "sha512-9Kc6tMaAPZKTGevdfcvx5zT3v4BTfamo+djJE29wF6ds1QLhoA09MZNDpWMZaebWzuoOTIXhDvgmqmjSlUOGlw==", + "license": "MIT", + "peer": true + }, "node_modules/@hookform/resolvers": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", @@ -3271,7 +3298,6 @@ "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3281,7 +3307,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -3292,7 +3317,6 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3349,7 +3373,6 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -3608,7 +3631,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3748,7 +3770,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -4300,7 +4321,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4750,6 +4770,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -4760,6 +4789,43 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/i18next": { + "version": "26.0.8", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.8.tgz", + "integrity": "sha512-BRzLom0mhDhV9v0QhgUUHWQJuwFmnr1194xEcNLYD6ym8y8s542n4jXUvRLnhNTbh9PmpU6kGZamyuGHQMsGjw==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-icu": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/i18next-icu/-/i18next-icu-2.4.3.tgz", + "integrity": "sha512-Clb5XCp416Z+BkJUTATCjmDcw2AFzSUDVLxLVK/KhtXP6TJQHrht+6MqoJU1hCpyoCBKe59wMO9pvCvYroNcKg==", + "license": "MIT", + "peerDependencies": { + "intl-messageformat": ">=10.3.3 <12.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4818,6 +4884,17 @@ "node": ">=12" } }, + "node_modules/intl-messageformat": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.2.tgz", + "integrity": "sha512-yUfyIkPGqMvvk2onw2xBJeLsjXdiYUYebR8mmZVQYBuZUJsFGVht48Ftm1khgu8EZ0n+izX4rAEj3fLAilkh9g==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@formatjs/fast-memoize": "3.1.2", + "@formatjs/icu-messageformat-parser": "3.5.5" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -6550,7 +6627,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6560,7 +6636,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -6573,7 +6648,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.65.0.tgz", "integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -6585,6 +6659,33 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-i18next": { + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.6.tgz", + "integrity": "sha512-WzJ6SMKF+GTD7JZZqxSR1AKKmXjaSu39sClUrNlwxS4Tl7a99O+ltFy6yhPMO+wgZuxpQjJ2PZkfrQKmAqrLhw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.0.1", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -7117,8 +7218,7 @@ "version": "4.1.16", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz", "integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", @@ -7139,7 +7239,6 @@ "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", "devOptional": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -7197,7 +7296,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7284,9 +7382,8 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7590,7 +7687,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -7682,7 +7778,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7690,6 +7785,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 3f3f371..c8e4ca7 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,14 @@ { "name": "neo-ui-framework", "private": true, - "version": "3.2.1", + "version": "4.1.0", "type": "module", + "engines": { + "node": "22.x" + }, "scripts": { "dev": "vite", - "build": "tsc -b && vite build --mode production", + "build": "./scripts/build-with-node22.sh", "lint": "eslint .", "preview": "vite preview" }, @@ -39,6 +42,8 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "cookie": "^1.0.2", + "i18next": "^26.0.8", + "i18next-icu": "^2.4.3", "js-yaml": "^4.1.1", "lucide-react": "^0.548.0", "next-themes": "^0.4.6", @@ -46,6 +51,7 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-hook-form": "^7.65.0", + "react-i18next": "^17.0.6", "react-markdown": "^10.1.0", "react-router-dom": "^7.9.4", "recharts": "^2.15.4", @@ -75,4 +81,4 @@ "typescript-eslint": "^8.45.0", "vite": "^7.1.7" } -} \ No newline at end of file +} diff --git a/scripts/build-with-node22.sh b/scripts/build-with-node22.sh new file mode 100755 index 0000000..b0677e6 --- /dev/null +++ b/scripts/build-with-node22.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -euo pipefail + +export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" + +if ! command -v nvm >/dev/null 2>&1; then + if [ -s "$NVM_DIR/nvm.sh" ]; then + # shellcheck disable=SC1090 + . "$NVM_DIR/nvm.sh" + fi +fi + +if ! command -v nvm >/dev/null 2>&1; then + echo "Error: nvm is required to run builds." + echo "Install nvm and retry, or run the build from a shell where nvm is already loaded." + exit 1 +fi + +nvm use 22 >/dev/null + +if [[ "$(node -v)" != v22.* ]]; then + echo "Error: expected Node 22 after 'nvm use 22', got $(node -v)." + exit 1 +fi + +tsc -b +vite build --mode production \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index e0444a4..863cc88 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,32 +6,57 @@ import { ThemeProvider } from "@/components/navs/theme-provider" import { AppSidebar } from "@/components/sidebars/sidebar" import { SiteHeader } from "@/components/sidebars/header" import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar" +import { Spinner } from "@/components/ui/spinner" // import Connector from "@/components/pages/connector" -import Monitoring from "@/components/pages/monitoring" -import Shares from "@/components/pages/shares" -import Files from "@/components/pages/files" -import Tasks from "@/components/pages/tasks" -import Users from "@/components/pages/users" -import Help from "@/components/pages/help" -import Logs from "@/components/pages/logs" -import Settings from "@/components/pages/settings" +const Monitoring = React.lazy(() => import("@/components/pages/monitoring")) +const Shares = React.lazy(() => import("@/components/pages/shares")) +const Files = React.lazy(() => import("@/components/pages/files")) +const Tasks = React.lazy(() => import("@/components/pages/tasks")) +const Users = React.lazy(() => import("@/components/pages/users")) +const Help = React.lazy(() => import("@/components/pages/help")) +const Logs = React.lazy(() => import("@/components/pages/logs")) +const Settings = React.lazy(() => import("@/components/pages/settings")) const MyDatasets = React.lazy(() => import("@/components/pages/my-datasets")) const ContentSearch = React.lazy(() => import("@/components/pages/content-search")) const DatasetPage = React.lazy(() => import("@/components/pages/dataset-page")) +const Entities = React.lazy(() => import("@/components/pages/entities")) import LoginPage from "@/components/pages/login-page" +import OAuthCallbackPage from "@/components/pages/oauth-callback" import { useNeoApi } from "@/hooks/useNeoApi" import { SetupWizardDialog } from "@/components/dialogs/setup-wizard-dialog" +function RouteFallback() { + return ( +
+ +
+ ) +} + function App() { const { state, handlers } = useNeoApi() if (!state.token) { return ( - + + + } /> + + } + /> + + { }} // Controlled by state @@ -57,7 +82,6 @@ function App() { isConnected={!!state.token} onConnect={handlers.handleConnect} onLogout={handlers.handleLogout} - datasets={state.datasets} /> - - - } - /> - {/* - } - /> */} - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - } /> - }> + + + } + /> + {/* + } + /> */} + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + } + /> + + } + /> + + } + /> + } + /> + + } /> - } /> - + } /> - } /> - } /> - + } /> + +
diff --git a/src/components/cards/acl-cache-card.tsx b/src/components/cards/acl-cache-card.tsx index b269a2d..0baff36 100644 --- a/src/components/cards/acl-cache-card.tsx +++ b/src/components/cards/acl-cache-card.tsx @@ -1,6 +1,7 @@ // Copyright 2025 NetApp, Inc. All Rights Reserved. "use client" +import { useTranslation } from "react-i18next" import { Server, Database, @@ -24,6 +25,7 @@ interface AclCacheCardProps { } export function AclCacheCard({ stats, className }: AclCacheCardProps) { + const { t } = useTranslation() if (!stats) return null // Determine status color @@ -42,14 +44,14 @@ export function AclCacheCard({ stats, className }: AclCacheCardProps) {
- ACL Cache Stats + {t("aclCacheStats", { ns: "monitoring" })}
{stats.status.toUpperCase()}
- Access Control List cache performance + {t("aclCacheDescription", { ns: "monitoring" })} @@ -58,9 +60,9 @@ export function AclCacheCard({ stats, className }: AclCacheCardProps) {
- Capacity Used + {t("capacityUsed", { ns: "monitoring" })}
- {stats.capacity_used_percent.toFixed(1)}% + {stats.capacity_used_percent.toFixed(1)}%
{/* Progress bar for capacity */}
@@ -70,8 +72,8 @@ export function AclCacheCard({ stats, className }: AclCacheCardProps) { />
- {stats.size} items - Max: {stats.max_size} + {stats.size} items + {t("maxSizeLabel", { ns: "monitoring" })}: {stats.max_size}
@@ -81,25 +83,33 @@ export function AclCacheCard({ stats, className }: AclCacheCardProps) {

- Performance + {t("performance", { ns: "monitoring" })}

-
Hit Rate
-
{(stats.hit_rate * 100).toFixed(1)}%
+
{t("hitRate", { ns: "monitoring" })}
+
+ {(stats.hit_rate * 100).toFixed(1)}% +
-
Total Requests
-
{stats.total_requests.toLocaleString()}
+
{t("totalRequests", { ns: "monitoring" })}
+
+ {stats.total_requests.toLocaleString()} +
-
Hits
-
{stats.hits.toLocaleString()}
+
{t("hits", { ns: "monitoring" })}
+
+ {stats.hits.toLocaleString()} +
-
Misses
-
{stats.misses.toLocaleString()}
+
{t("misses", { ns: "monitoring" })}
+
+ {stats.misses.toLocaleString()} +
@@ -111,7 +121,7 @@ export function AclCacheCard({ stats, className }: AclCacheCardProps) {

- Recommendations + {t("recommendations", { ns: "monitoring" })}

    {stats.recommendations.map((rec, i) => ( diff --git a/src/components/cards/cache-stats-card.tsx b/src/components/cards/cache-stats-card.tsx index f71aa2f..4cdb3a8 100644 --- a/src/components/cards/cache-stats-card.tsx +++ b/src/components/cards/cache-stats-card.tsx @@ -1,12 +1,14 @@ // Copyright 2025 NetApp, Inc. All Rights Reserved. "use client" +import { useTranslation } from "react-i18next" import { Card, CardContent, CardHeader, CardTitle, } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" import { Separator } from "@/components/ui/separator" import { Clock, HardDrive } from "lucide-react" import { IconTable } from "@tabler/icons-react" @@ -21,6 +23,7 @@ interface CacheStatsCardProps { } export function CacheStatsCard({ cacheStats, className }: CacheStatsCardProps) { + const { t } = useTranslation() const { monitoringTtl, filesTtl, cacheMaxSize } = useSettings() const formatBytes = (bytes: number) => { @@ -36,7 +39,7 @@ export function CacheStatsCard({ cacheStats, className }: CacheStatsCardProps) { - Cache Statistics + {t("cacheStatistics", { ns: "monitoring" })} @@ -44,26 +47,32 @@ export function CacheStatsCard({ cacheStats, className }: CacheStatsCardProps) {
    - TTL Configuration + {t("ttlConfiguration", { ns: "monitoring" })}
    - Monitoring: - {monitoringTtl} min + {t("monitoringLabel", { ns: "monitoring" })}: + + {monitoringTtl} {t("minSuffix", { ns: "monitoring" })} +
    - Files: - {filesTtl} min + {t("filesLabel", { ns: "monitoring" })}: + + {filesTtl} {t("minSuffix", { ns: "monitoring" })} +
    - Storage Limit + {t("storageLimit", { ns: "monitoring" })}
    - Max Size: - {cacheMaxSize} MB + {t("maxSizeLabel", { ns: "monitoring" })}: + + {cacheMaxSize} MB +
    @@ -73,15 +82,19 @@ export function CacheStatsCard({ cacheStats, className }: CacheStatsCardProps) {
    - Current Usage + {t("currentUsage", { ns: "monitoring" })}
    - Size: - {formatBytes(cacheStats?.sizeBytes || 0)} + {t("sizeLabel", { ns: "monitoring" })}: + + {formatBytes(cacheStats?.sizeBytes || 0)} +
    - Items: - {cacheStats?.items || 0} + {t("itemsLabel", { ns: "monitoring" })}: + + {cacheStats?.items || 0} +
diff --git a/src/components/cards/neo-instance-card.tsx b/src/components/cards/neo-instance-card.tsx index 2fa38c0..cf6e1c4 100644 --- a/src/components/cards/neo-instance-card.tsx +++ b/src/components/cards/neo-instance-card.tsx @@ -1,11 +1,11 @@ // Copyright 2025 NetApp, Inc. All Rights Reserved. "use client" +import { useTranslation } from "react-i18next" import { Activity, CheckIcon, AlertCircleIcon, - HardDrive } from "lucide-react" import { Card, @@ -17,7 +17,7 @@ import { import { Badge } from "@/components/ui/badge" import { Separator } from "@/components/ui/separator" import type { HealthResponse, LicenseResponse } from "@/services/neo-api" -import { IconCpu, IconRuler3, IconArrowsJoin } from "@tabler/icons-react" +import { IconArrowsJoin } from "@tabler/icons-react" interface NeoInstanceCardProps { health: HealthResponse | null @@ -26,23 +26,19 @@ interface NeoInstanceCardProps { } export function NeoInstanceCard({ health, license, className }: NeoInstanceCardProps) { - const healthStatus = health?.status ?? "Not connected" - const healthComponents = [ - { key: "database", label: "Database" }, - { key: "filesystem", label: "Filesystem" }, - { key: "graph_connector", label: "Graph Connector" }, - { key: "shares", label: "Shares" }, - ] + const { t } = useTranslation() + const healthStatus = health?.status ?? t("notConnected", { ns: "monitoring" }) + const healthComponents = Object.entries(health?.components ?? {}) return ( - Neo Instance + {t("neoInstance", { ns: "monitoring" })} - Health and resource information + {t("healthAndResourceInfo", { ns: "monitoring" })} @@ -51,7 +47,7 @@ export function NeoInstanceCard({ health, license, className }: NeoInstanceCardP
- Status + {t("statusLabel", { ns: "monitoring" })}
- License + {t("licenseLabel", { ns: "monitoring" })}
{ - const days = license?.details.days_remaining + const days = license?.details?.days_remaining if (typeof days !== 'number') return "" if (days < 10) return "text-destructive border-destructive/50" return "text-green-600 border-green-200 dark:text-green-400 dark:border-green-800" })()}`} > - {license?.details.days_remaining ?? "Unknown"} days + {license?.details?.days_remaining ?? t("unknown", { ns: "monitoring" })} {t("daysSuffix", { ns: "monitoring" })}
@@ -91,78 +87,50 @@ export function NeoInstanceCard({ health, license, className }: NeoInstanceCardP {/* Resource Metrics */} -
-

- - Resource Usage -

-
-
- -

{health?.metrics?.cpu_percent?.toFixed(1) ?? "0.0"}%

-

CPU

-
-
- -

{health?.metrics?.memory_percent?.toFixed(1) ?? "0.0"}%

-

Memory

-
-
- -

{health?.metrics?.disk_percent?.toFixed(1) ?? "0.0"}%

-

Disk

-
-
-
+ {healthComponents.length > 0 && ( + <> + {/* Component Status Table */} +
+

{t("componentHealth", { ns: "monitoring" })}

+
+ {healthComponents.map(([key, value]) => { + const isHealthy = value === "ok" || value === "healthy" + const isNotConfigured = value === "not_configured" - + let badgeClass = "text-destructive border-destructive/50" + let Icon = AlertCircleIcon - {/* Component Status Table */} -
-

Component Health

-
- {healthComponents.map(({ key, label }) => { - if (!health || !health.components) return null - const component = health.components[key as keyof typeof health.components] - let isHealthy = false - let isNotConfigured = false - let statusText = "Unknown" + if (isHealthy) { + badgeClass = "text-green-600 border-green-200 dark:text-green-400 dark:border-green-800" + Icon = CheckIcon + } else if (isNotConfigured) { + badgeClass = "text-orange-600 border-orange-200 dark:text-orange-400 dark:border-orange-800" + Icon = AlertCircleIcon + } - if (key === "shares" && component && 'active_count' in component) { - isHealthy = component.errors.length === 0 - statusText = isHealthy ? "Healthy" : "Errors" - } else if (component && 'status' in component) { - isHealthy = component.status === "healthy" - isNotConfigured = component.status === "not_configured" - statusText = component.status - } + const statusText = isHealthy + ? t("healthy", { ns: "monitoring" }) + : isNotConfigured + ? t("notConfigured", { ns: "monitoring" }) + : value - let badgeClass = "text-destructive border-destructive/50" - let Icon = AlertCircleIcon - - if (isHealthy) { - badgeClass = "text-green-600 border-green-200 dark:text-green-400 dark:border-green-800" - Icon = CheckIcon - } else if (isNotConfigured) { - badgeClass = "text-orange-600 border-orange-200 dark:text-orange-400 dark:border-orange-800" - Icon = AlertCircleIcon - } - - return ( -
- {label}: - - - {statusText} - -
- ) - })} -
-
+ return ( +
+ {key.replace(/_/g, " ")}: + + + {statusText} + +
+ ) + })} +
+
+ + )} ) diff --git a/src/components/cards/overview-card.tsx b/src/components/cards/overview-card.tsx index 9b804b8..173564b 100644 --- a/src/components/cards/overview-card.tsx +++ b/src/components/cards/overview-card.tsx @@ -70,7 +70,7 @@ export function OverviewCard({ Caching Strategy
-
LRU
+
LRU
) : (
@@ -78,7 +78,7 @@ export function OverviewCard({ Monitoring TTL
-
{monitoringTtl} min
+
{monitoringTtl} min
)}
@@ -86,14 +86,14 @@ export function OverviewCard({ Files TTL
-
{filesTtl} min
+
{filesTtl} min
Cache Size Limit
-
{cacheMaxSize} MB
+
{cacheMaxSize} MB
@@ -101,7 +101,7 @@ export function OverviewCard({ Current Usage
-
{formatBytes(cacheStats?.sizeBytes || 0)}
+
{formatBytes(cacheStats?.sizeBytes || 0)}
{cacheStats?.items || 0} items
diff --git a/src/components/cards/tasks-summary-card.tsx b/src/components/cards/tasks-summary-card.tsx index d83e954..ff4fd94 100644 --- a/src/components/cards/tasks-summary-card.tsx +++ b/src/components/cards/tasks-summary-card.tsx @@ -1,6 +1,7 @@ // Copyright 2025 NetApp, Inc. All Rights Reserved. "use client" +import { useTranslation } from "react-i18next" import { ListTodo, CheckCircle2, @@ -16,6 +17,7 @@ import { CardHeader, CardTitle, } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" import { Separator } from "@/components/ui/separator" import type { TaskStatisticsResponse } from "@/services/models" @@ -25,6 +27,7 @@ interface TasksSummaryCardProps { } export function TasksSummaryCard({ stats, className }: TasksSummaryCardProps) { + const { t } = useTranslation() if (!stats) return null return ( @@ -32,10 +35,10 @@ export function TasksSummaryCard({ stats, className }: TasksSummaryCardProps) { - Tasks Summary + {t("tasksSummary", { ns: "monitoring" })} - Overview of background task execution status + {t("tasksSummaryDescription", { ns: "monitoring" })} @@ -43,9 +46,11 @@ export function TasksSummaryCard({ stats, className }: TasksSummaryCardProps) {
- Total Tasks + {t("totalTasks", { ns: "monitoring" })} +
+
+ {stats.total_tasks}
-
{stats.total_tasks}
@@ -53,7 +58,7 @@ export function TasksSummaryCard({ stats, className }: TasksSummaryCardProps) { {/* Task Status Breakdown */}

- Status Breakdown + {t("statusBreakdown", { ns: "monitoring" })}

@@ -61,55 +66,55 @@ export function TasksSummaryCard({ stats, className }: TasksSummaryCardProps) {
- Running + {t("running", { ns: "monitoring" })}
- + {stats.by_status.running} - +
{/* Pending */}
- Pending + {t("pending", { ns: "monitoring" })}
- + {stats.by_status.pending} - +
{/* Completed */}
- Completed + {t("completed", { ns: "monitoring" })}
- + {stats.by_status.completed} - +
{/* Failed */}
- Failed + {t("failed", { ns: "monitoring" })}
- + {stats.by_status.failed} - +
{/* Cancelled */}
- Cancelled + {t("cancelled", { ns: "monitoring" })}
- + {stats.by_status.cancelled} - +
diff --git a/src/components/cards/versioning-card.tsx b/src/components/cards/versioning-card.tsx index 485860c..540550b 100644 --- a/src/components/cards/versioning-card.tsx +++ b/src/components/cards/versioning-card.tsx @@ -2,10 +2,14 @@ "use client" import { useEffect, useState } from "react" +import { useTranslation } from "react-i18next" import { GitBranch, - Box + Box, + Activity, + CheckIcon, + AlertCircleIcon, } from "lucide-react" import { Card, @@ -16,23 +20,25 @@ import { } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Separator } from "@/components/ui/separator" -import type { VersionResponse, HelmChartVersionResponse } from "@/services/neo-api" +import type { VersionResponse, HelmChartVersionResponse, HealthResponse, LicenseResponse } from "@/services/neo-api" import { IconDeviceDesktop, IconLayersDifference, IconPackage } from "@tabler/icons-react" interface VersioningCardProps { version: VersionResponse | null helmChartVersion: HelmChartVersionResponse | null + health: HealthResponse | null + license: LicenseResponse | null className?: string } -export function VersioningCard({ version, helmChartVersion, className }: VersioningCardProps) { - const versionLabel = version?.version ?? "Unknown" - const buildDateLabel = version?.build_date - ? version.build_date.split("T")[0] ?? "Unknown" - : "Unknown" - const latestAppVersion = helmChartVersion?.app_version ?? "Checking..." - const latestChartVersion = helmChartVersion?.chart_version ?? "Checking..." - const [latestUiVersion, setLatestUiVersion] = useState("Checking...") +export function VersioningCard({ version, helmChartVersion, health, license, className }: VersioningCardProps) { + const { t } = useTranslation() + const versionLabel = version?.version ?? t("unknown", { ns: "monitoring" }) + const healthStatus = health?.status ?? t("notConnected", { ns: "monitoring" }) + const healthComponents = Object.entries(health?.components ?? {}) + const latestAppVersion = helmChartVersion?.app_version ?? t("checking", { ns: "monitoring" }) + const latestChartVersion = helmChartVersion?.chart_version ?? t("checking", { ns: "monitoring" }) + const [latestUiVersion, setLatestUiVersion] = useState(t("checking", { ns: "monitoring" })) useEffect(() => { const fetchLatestVersion = async () => { @@ -51,7 +57,7 @@ export function VersioningCard({ version, helmChartVersion, className }: Version } else { setLatestUiVersion("n/a") } - } catch (error) { + } catch { setLatestUiVersion("n/a") } } @@ -64,24 +70,23 @@ export function VersioningCard({ version, helmChartVersion, className }: Version - System Versions + {t("systemVersions", { ns: "monitoring" })} - Software versioning and release information + {t("softwareVersioning", { ns: "monitoring" })} {/* Current Versions */} -
+
- Neo Core + Core
-
-
{versionLabel}
- - {buildDateLabel} +
+ + {versionLabel}
@@ -89,28 +94,120 @@ export function VersioningCard({ version, helmChartVersion, className }: Version
- UI Framework + Console
- +
+
+ + + + {/* System Status & License Overview */} +
+
+
+ + {t("statusLabel", { ns: "monitoring" })} +
+
+ - {__APP_VERSION__} - + {healthStatus} + +
+
+ +
+
+ + {t("licenseLabel", { ns: "monitoring" })} +
+
+ { + const days = license?.details?.days_remaining + if (typeof days !== "number") return "" + if (days < 10) return "text-destructive border-destructive/50" + return "text-green-600 border-green-200 dark:text-green-400 dark:border-green-800" + })()}`} + > + {license?.details?.days_remaining ?? t("unknown", { ns: "monitoring" })} {t("daysSuffix", { ns: "monitoring" })} +
+ {healthComponents.length > 0 && ( + <> + + + {/* Component Status */} +
+

{t("componentHealth", { ns: "monitoring" })}

+
+ {healthComponents.map(([key, value]) => { + const isHealthy = value === "ok" || value === "healthy" + const isNotConfigured = value === "not_configured" + + let badgeClass = "text-destructive border-destructive/50" + let Icon = AlertCircleIcon + + if (isHealthy) { + badgeClass = "text-green-600 border-green-200 dark:text-green-400 dark:border-green-800" + Icon = CheckIcon + } else if (isNotConfigured) { + badgeClass = "text-orange-600 border-orange-200 dark:text-orange-400 dark:border-orange-800" + Icon = AlertCircleIcon + } + + const statusText = isHealthy + ? t("healthy", { ns: "monitoring" }) + : isNotConfigured + ? t("notConfigured", { ns: "monitoring" }) + : value + + return ( +
+ {key.replace(/_/g, " ")}: + + + {statusText} + +
+ ) + })} +
+
+ + )} + {/* Latest Available Versions */}

- Latest Available Versions + {t("latestAvailableVersions", { ns: "monitoring" })}

@@ -118,38 +215,40 @@ export function VersioningCard({ version, helmChartVersion, className }: Version
- Helm Chart + {t("helmChart", { ns: "monitoring" })}
- - {latestChartVersion} - + + + {latestChartVersion} + +
{/* Neo Core (was App Version) */}
- Neo Core + {t("neoCore", { ns: "monitoring" })}
- - {helmChartVersion?.app_version === "Unknown" ? "Unable to check" : latestAppVersion} - + + {helmChartVersion?.app_version === "Unknown" ? t("unableToCheck", { ns: "monitoring" }) : latestAppVersion} +
{/* UI Framework Placeholder */}
- UI Framework + {t("neoConsole", { ns: "monitoring" })}
- + {latestUiVersion} - +
diff --git a/src/components/charts/contentsavings.tsx b/src/components/charts/contentsavings.tsx index a592472..53f820f 100644 --- a/src/components/charts/contentsavings.tsx +++ b/src/components/charts/contentsavings.tsx @@ -2,6 +2,7 @@ "use client" import * as React from "react" +import { useTranslation } from "react-i18next" import { TrendingDown } from "lucide-react" import { Label, Pie, PieChart } from "recharts" @@ -44,7 +45,18 @@ const chartConfig = { }, } satisfies ChartConfig +type ContentSavingsTooltipDatum = { + type: "content" | "savings" + size: number +} + +type ContentSavingsTooltipProps = { + active?: boolean + payload?: Array<{ payload: ContentSavingsTooltipDatum }> +} + export function ContentSavingsChart({ databaseSize, className }: ContentSavingsChartProps) { + const { t } = useTranslation() const chartData = React.useMemo(() => { if (!databaseSize) { return [] @@ -99,31 +111,31 @@ export function ContentSavingsChart({ databaseSize, className }: ContentSavingsC }, [databaseSize]) // Custom tooltip to show size details - const CustomTooltip = ({ active, payload }: any) => { - if (active && payload && payload.length && savingsInfo) { + const CustomTooltip = ({ active, payload }: ContentSavingsTooltipProps) => { + if (active && payload && payload.length > 0 && savingsInfo) { const data = payload[0].payload const percentage = ((data.size / totalOriginalSize) * 100).toFixed(1) return (

- {data.type === 'content' ? 'Content Size' : 'Space Saved'} + {data.type === 'content' ? t("contentSize", { ns: "monitoring" }) : t("spaceSaved", { ns: "monitoring" })}

{data.type === 'content' ? ( <> - Size: {data.size.toFixed(2)} MB ({percentage}%)
- Original Size: {savingsInfo.originalSize.toFixed(2)} MB + {t("sizeLabel", { ns: "monitoring" })}: {data.size.toFixed(2)} {t("mbSuffix", { ns: "monitoring" })} ({percentage}%)
+ {t("originalFiles", { ns: "monitoring" })}: {savingsInfo.originalSize.toFixed(2)} {t("mbSuffix", { ns: "monitoring" })} ) : ( <> - Size: {data.size.toFixed(2)} MB ({percentage}%) + {t("sizeLabel", { ns: "monitoring" })}: {data.size.toFixed(2)} {t("mbSuffix", { ns: "monitoring" })} ({percentage}%) )}

{data.type === 'savings' && (

- Compression ratio: {savingsInfo.compressionRatio}:1 + {t("compressionRatio", { ns: "monitoring" })}: {savingsInfo.compressionRatio}:1

)}
@@ -136,10 +148,10 @@ export function ContentSavingsChart({ databaseSize, className }: ContentSavingsC return ( - Content Savings + {t("contentSavings", { ns: "monitoring" })} -
No database data available
+
{t("noDatabaseDataAvailable", { ns: "monitoring" })}
) @@ -148,8 +160,8 @@ export function ContentSavingsChart({ databaseSize, className }: ContentSavingsC return ( - Content Extraction Efficiency - Original files vs extracted content size + {t("contentExtractionEfficiency", { ns: "monitoring" })} + {t("originalVsExtracted", { ns: "monitoring" })} - Space Saved + {t("spaceSaved", { ns: "monitoring" })} ) @@ -205,13 +217,13 @@ export function ContentSavingsChart({ databaseSize, className }: ContentSavingsC {savingsInfo && ( <>
- {savingsInfo.savings.toFixed(2)} MB saved ({savingsInfo.compressionRatio}:1 ratio) + {savingsInfo.savings.toFixed(2)} {t("mbSuffix", { ns: "monitoring" })} {t("spaceSaved", { ns: "monitoring" }).toLowerCase()} ({savingsInfo.compressionRatio}:1)
)}
- Storage efficiency through content extraction + {t("storageEfficiency", { ns: "monitoring" })}
diff --git a/src/components/charts/databasesize.tsx b/src/components/charts/databasesize.tsx index 7836b1e..54b06bf 100644 --- a/src/components/charts/databasesize.tsx +++ b/src/components/charts/databasesize.tsx @@ -1,6 +1,7 @@ // Copyright 2025 NetApp, Inc. All Rights Reserved. "use client" +import { useTranslation } from "react-i18next" import { Database, HardDrive, @@ -25,15 +26,16 @@ interface DatabaseSizeCardProps { } export function DatabaseSizeCard({ databaseSize, className }: DatabaseSizeCardProps) { + const { t } = useTranslation() if (!databaseSize) { return ( - Database Statistics + {t("databaseStatistics", { ns: "monitoring" })} -
Database information not available
+
{t("databaseInformationUnavailable", { ns: "monitoring" })}
) @@ -44,10 +46,10 @@ export function DatabaseSizeCard({ databaseSize, className }: DatabaseSizeCardPr - Database Statistics + {t("databaseStatistics", { ns: "monitoring" })} - Database size and content breakdown + {t("contentSizeBreakdown", { ns: "monitoring" })} @@ -56,23 +58,25 @@ export function DatabaseSizeCard({ databaseSize, className }: DatabaseSizeCardPr
- Database Size + Size +
+
+ + {databaseSize.database_size_info} +
-
{databaseSize.database_size_info}
-

- {databaseSize.database_file_size_bytes.toLocaleString()} bytes -

- Files Tracked + Items +
+
+ + {databaseSize.total_files_tracked.toLocaleString()} +
-
{databaseSize.total_files_tracked.toLocaleString()}
-

- Total: {databaseSize.total_original_file_size_mb.toFixed(2)} MB -

@@ -82,38 +86,38 @@ export function DatabaseSizeCard({ databaseSize, className }: DatabaseSizeCardPr

- Table Statistics + {t("tableStatistics", { ns: "monitoring" })}

{/* Shares */}
- Shares: - + {t("sharesLabel", { ns: "monitoring" })}: + {databaseSize.table_statistics?.shares?.row_count ?? 0}
{/* Users */}
- Users: - + {t("usersLabel", { ns: "monitoring" })}: + {databaseSize.table_statistics?.users?.row_count ?? 0}
{/* File Metadata */}
- File Metadata: - + {t("fileMetadata", { ns: "monitoring" })}: + {databaseSize.table_statistics?.file_metadata?.row_count ?? 0}
{/* Operations Log */}
- Operations Log: - + Ops: + {databaseSize.table_statistics?.operations_log?.row_count ?? 0}
@@ -124,32 +128,26 @@ export function DatabaseSizeCard({ databaseSize, className }: DatabaseSizeCardPr {/* Content Size Breakdown */}
-

Content Size Breakdown

+

{t("contentSizeBreakdown", { ns: "monitoring" })}

- Original Files: - {databaseSize.total_original_file_size_mb.toFixed(2)} MB + {t("originalFiles", { ns: "monitoring" })}: + {databaseSize.total_original_file_size_mb.toFixed(2)} {t("mbSuffix", { ns: "monitoring" })}
- Extracted Content: - {databaseSize.total_file_content_size_mb.toFixed(2)} MB + {t("extractedContent", { ns: "monitoring" })}: + {databaseSize.total_file_content_size_mb.toFixed(2)} {t("mbSuffix", { ns: "monitoring" })}
- Operations Log: - {databaseSize.table_statistics?.operations_log?.total_content_size_mb ?? "0.00"} MB + {t("operationsLog", { ns: "monitoring" })}: + {databaseSize.table_statistics?.operations_log?.total_content_size_mb ?? "0.00"} {t("mbSuffix", { ns: "monitoring" })}
- {/* Timestamp */} -
-

- Last updated: {new Date(databaseSize.timestamp).toLocaleString()} -

-
) diff --git a/src/components/charts/filetype.tsx b/src/components/charts/filetype.tsx index 93d1b9c..76f77c0 100644 --- a/src/components/charts/filetype.tsx +++ b/src/components/charts/filetype.tsx @@ -2,6 +2,7 @@ "use client" import * as React from "react" +import { useTranslation } from "react-i18next" import { TrendingUp, // File @@ -63,6 +64,7 @@ const chartConfig = { } satisfies ChartConfig export function FileTypeChart({ fileAnalytics }: FileTypeChartProps) { + const { t } = useTranslation() const chartData = React.useMemo(() => { if (!fileAnalytics || fileAnalytics.length === 0) { return [] @@ -89,10 +91,10 @@ export function FileTypeChart({ fileAnalytics }: FileTypeChartProps) { return ( - Document Types + {t("documentTypes", { ns: "monitoring" })} -
No document data available
+
{t("noDocumentDataAvailable", { ns: "monitoring" })}
) @@ -101,8 +103,8 @@ export function FileTypeChart({ fileAnalytics }: FileTypeChartProps) { return ( - Document Types Distribution - Breakdown by document type (PDF, DOC, PPT, TXT) + {t("documentTypesDistribution", { ns: "monitoring" })} + {t("breakdownByDocumentType", { ns: "monitoring" })} - Documents + {t("documents", { ns: "monitoring" })} ) @@ -157,12 +159,12 @@ export function FileTypeChart({ fileAnalytics }: FileTypeChartProps) { {mostCommonType && (
- Most common: {mostCommonType.fileType.toUpperCase()} ({mostCommonType.count.toLocaleString()} files) + {t("mostCommon", { ns: "monitoring" })}: {mostCommonType.fileType.toUpperCase()} ({mostCommonType.count.toLocaleString()} {t("fileCountSuffix", { ns: "monitoring" })})
)}
- Document type distribution across all indexed shares + {t("documentTypeDistributionFooter", { ns: "monitoring" })}
diff --git a/src/components/charts/sharesdistribution.tsx b/src/components/charts/sharesdistribution.tsx index 0a479ae..f47a871 100644 --- a/src/components/charts/sharesdistribution.tsx +++ b/src/components/charts/sharesdistribution.tsx @@ -2,6 +2,7 @@ "use client" import * as React from "react" +import { useTranslation } from "react-i18next" import { TrendingUp, FolderOpen } from "lucide-react" import { Label, Pie, PieChart } from "recharts" @@ -52,7 +53,21 @@ const generateChartConfig = (sharesAnalytics: { share_id: string; share_name: st return config } + +type SharesDistributionTooltipDatum = { + shareName: string + shareFullPath: string + count: number + totalSize: number +} + +type SharesDistributionTooltipProps = { + active?: boolean + payload?: Array<{ payload: SharesDistributionTooltipDatum }> +} + export function SharesDistributionChart({ sharesAnalytics }: SharesDistributionChartProps) { + const { t } = useTranslation() const chartData = React.useMemo(() => { if (!sharesAnalytics || sharesAnalytics.length === 0) { return [] @@ -86,8 +101,8 @@ export function SharesDistributionChart({ sharesAnalytics }: SharesDistributionC }, [chartData]) // Custom tooltip to show share details - const CustomTooltip = ({ active, payload }: any) => { - if (active && payload && payload.length) { + const CustomTooltip = ({ active, payload }: SharesDistributionTooltipProps) => { + if (active && payload && payload.length > 0) { const data = payload[0].payload const percentage = ((data.count / totalFiles) * 100).toFixed(1) const sizeInMB = (data.totalSize / (1024 * 1024)).toFixed(1) @@ -97,10 +112,10 @@ export function SharesDistributionChart({ sharesAnalytics }: SharesDistributionC

{data.shareName}

{data.shareFullPath}

- Files: {data.count.toLocaleString()} ({percentage}%) + {t("filesLabel", { ns: "monitoring" })}: {data.count.toLocaleString()} ({percentage}%)

- Size: {sizeInMB} MB + {t("sizeLabel", { ns: "monitoring" })}: {sizeInMB} {t("mbSuffix", { ns: "monitoring" })}

) @@ -112,11 +127,11 @@ export function SharesDistributionChart({ sharesAnalytics }: SharesDistributionC return ( - Document Distribution by Shares + {t("documentDistributionByShares", { ns: "monitoring" })} -
No shares analytics data available
+
{t("noSharesAnalyticsDataAvailable", { ns: "monitoring" })}
) @@ -125,8 +140,8 @@ export function SharesDistributionChart({ sharesAnalytics }: SharesDistributionC return ( - Document Distribution by Shares - File count breakdown across {chartData.length} active shares + {t("documentDistributionByShares", { ns: "monitoring" })} + {t("fileCountBreakdownAcrossShares", { ns: "monitoring", count: chartData.length })} - Total Files + {t("totalFiles", { ns: "monitoring" })} ) @@ -181,12 +196,12 @@ export function SharesDistributionChart({ sharesAnalytics }: SharesDistributionC {largestShare && (
- Largest share: {largestShare.shareName} ({largestShare.count.toLocaleString()} files) + {t("largestShare", { ns: "monitoring" })}: {largestShare.shareName} ({largestShare.count.toLocaleString()} {t("fileCountSuffix", { ns: "monitoring" })})
)}
- Distribution of {totalFiles.toLocaleString()} files across {chartData.length} indexed shares + {t("indexedSharesDistribution", { ns: "monitoring", files: totalFiles.toLocaleString(), shares: chartData.length })}
diff --git a/src/components/data-tables/filesT.tsx b/src/components/data-tables/filesT.tsx index 4b7bc9e..bf62a5a 100644 --- a/src/components/data-tables/filesT.tsx +++ b/src/components/data-tables/filesT.tsx @@ -55,7 +55,7 @@ export function FilesTable({ onPageChange, onFileClick }: FilesTableProps) { - const rows = files?.files ?? [] + const rows = useMemo(() => files?.files ?? [], [files?.files]) const message = emptyMessage ?? (loading ? "Loading files…" : "No files available.") const showShareColumn = rows.some((file) => file.share_name || file.share_path) const columnCount = 4 + (showShareColumn ? 1 : 0) @@ -180,11 +180,11 @@ export function FilesTable({ // Calculate pagination info const currentPage = files?.page ?? 1 // Change from 0 to 1 as default - const totalPages = files?.total_pages ?? 0 + const totalPages = files?.total_pages ?? null const hasPrevious = files?.has_previous ?? false const hasNext = files?.has_next ?? false - const totalCount = files?.total_count ?? 0 - const totalSize = files?.total_size ?? 0 + const totalCount = files?.total_count + const totalSize = files?.total_size return ( <> @@ -294,10 +294,10 @@ export function FilesTable({
{/* Pagination Controls */} - {files && !loading && totalPages > 1 ? ( + {files && !loading && (totalPages !== null ? totalPages > 1 : hasPrevious || hasNext) ? (
- Showing page {currentPage} of {totalPages} · {totalCount.toLocaleString()} files · Total size {totalSize.toLocaleString()} bytes + {totalPages !== null ? `Showing page ${currentPage} of ${totalPages}` : `Showing page ${currentPage}`} · {totalCount !== null && totalCount !== undefined ? `${totalCount.toLocaleString()} files` : "Count unavailable"} · {totalSize !== null && totalSize !== undefined ? `Total size ${totalSize.toLocaleString()} bytes` : "Size unavailable"}
- ) : files && !loading && totalCount > 0 ? ( + ) : files && !loading && typeof totalCount === "number" && totalCount > 0 ? (

- Showing page {currentPage} of {totalPages} · {totalCount.toLocaleString()} files · Total size {totalSize.toLocaleString()} bytes + {totalPages !== null ? `Showing page ${currentPage} of ${totalPages}` : `Showing page ${currentPage}`} · {totalCount.toLocaleString()} files · {typeof totalSize === "number" ? `Total size ${totalSize.toLocaleString()} bytes` : "Size unavailable"}

) : null} diff --git a/src/components/data-tables/logsT.tsx b/src/components/data-tables/logsT.tsx index 020d899..e99873f 100644 --- a/src/components/data-tables/logsT.tsx +++ b/src/components/data-tables/logsT.tsx @@ -1,6 +1,8 @@ // Copyright 2025 NetApp, Inc. All Rights Reserved. "use client" +import { useMemo, useState } from "react" + import { Badge } from "@/components/ui/badge" import { Table, @@ -25,6 +27,87 @@ const levelVariant: Record { + if (!isOpen) return null + return truncateLogContent(value) + }, [isOpen, value]) + + return ( +
setIsOpen(event.currentTarget.open)}> + + {label} + + {isOpen && displayValue ? ( + <> + {displayValue.truncated ? ( +

+ Display capped at {MAX_LOG_DETAIL_CHARS.toLocaleString()} characters. {displayValue.hiddenChars.toLocaleString()} additional characters omitted. +

+ ) : null} +
+            {displayValue.content}
+          
+ + ) : null} +
+ ) +} + +function LogContextDetails({ context }: { context: Record }) { + const [isOpen, setIsOpen] = useState(false) + const serializedContext = useMemo(() => { + if (!isOpen) return null + + try { + return truncateLogContent(JSON.stringify(context, null, 2)) + } catch { + return truncateLogContent("[unserializable log context]") + } + }, [context, isOpen]) + + return ( +
setIsOpen(event.currentTarget.open)}> + + View context + + {isOpen && serializedContext ? ( + <> + {serializedContext.truncated ? ( +

+ Display capped at {MAX_LOG_DETAIL_CHARS.toLocaleString()} characters. {serializedContext.hiddenChars.toLocaleString()} additional characters omitted. +

+ ) : null} +
+            {serializedContext.content}
+          
+ + ) : null} +
+ ) +} + export function LogsTable({ logs, loading = false }: LogsTableProps) { if (loading) { return ( @@ -60,23 +143,9 @@ export function LogsTable({ logs, loading = false }: LogsTableProps) { {log.message} {log.details ? ( -
- - View - -
-                        {log.details}
-                      
-
+ ) : log.context ? ( -
- - View context - -
-                        {JSON.stringify(log.context, null, 2)}
-                      
-
+ ) : ( "—" )} diff --git a/src/components/data-tables/monitoringT.tsx b/src/components/data-tables/monitoringT.tsx index bb1d726..fb33950 100644 --- a/src/components/data-tables/monitoringT.tsx +++ b/src/components/data-tables/monitoringT.tsx @@ -1,7 +1,8 @@ // Copyright 2025 NetApp, Inc. All Rights Reserved. "use client" -import { useEffect, useState } from "react" +import { useEffect, useMemo, useState } from "react" +import { useTranslation } from "react-i18next" import { IconAlertTriangle, IconClock, @@ -25,6 +26,8 @@ import { import { Badge } from "@/components/ui/badge" import { Progress } from "@/components/ui/progress" import { Separator } from "@/components/ui/separator" +import { NeoApiService } from "@/services/neo-api" +import { useNeoApi } from "@/hooks/useNeoApi" import type { MonitoringOverviewResponse, MonitoringWorkersResponse, @@ -44,7 +47,6 @@ import { FileTypeChart } from "@/components/charts/filetype" import { SharesDistributionChart } from "@/components/charts/sharesdistribution" import { DatabaseSizeCard } from "@/components/charts/databasesize" import { ContentSavingsChart } from "@/components/charts/contentsavings" -import { NeoInstanceCard } from "@/components/cards/neo-instance-card" import { VersioningCard } from "@/components/cards/versioning-card" import { CacheStatsCard } from "@/components/cards/cache-stats-card" import { TasksSummaryCard } from "@/components/cards/tasks-summary-card" @@ -87,6 +89,9 @@ export function MonitoringChart({ helmChartVersion, cacheStats }: MonitoringChartProps) { + const { t } = useTranslation() + const { state } = useNeoApi() + const token = state.token const { overview, workers, @@ -99,8 +104,55 @@ export function MonitoringChart({ sharesAnalytics } = monitoring + const staleWorkers = workers?.stale_workers + ?? workers?.workers?.filter((worker) => worker.status === "stale").length + ?? 0 + const stoppingWorkers = workers?.stopping_workers + ?? workers?.workers?.filter((worker) => worker.status === "stopping").length + ?? 0 + const stoppedWorkers = workers?.stopped_workers + ?? workers?.workers?.filter((worker) => worker.status === "stopped").length + ?? 0 + + const workQueue = overview?.work_queue + const pendingItems = workQueue?.total_pending ?? workQueue?.pending_items ?? 0 + const claimedItems = workQueue?.total_claimed ?? workQueue?.claimed_items ?? 0 + const processingItems = workQueue?.total_processing ?? workQueue?.processing_items ?? 0 + const failedItemsCount = workQueue?.total_failed ?? workQueue?.failed_items ?? 0 + const abandonedItems = workQueue?.total_abandoned ?? workQueue?.abandoned_items ?? 0 + const totalItems = workQueue?.total_items ?? (pendingItems + claimedItems + processingItems + failedItemsCount + abandonedItems) + const handleRetryWorkItems = onRetryWorkItems const [isRetrying, setIsRetrying] = useState(false) + const [isLoadingNerStatus, setIsLoadingNerStatus] = useState(false) + const [nerStatusError, setNerStatusError] = useState(null) + const [nerStatus, setNerStatus] = useState | null>(null) + + const normalizedNerStatus = useMemo(() => { + if (!nerStatus) return null + + const statusValue = typeof nerStatus.status === "string" + ? nerStatus.status.toLowerCase() + : undefined + const runningValue = typeof nerStatus.running === "boolean" + ? nerStatus.running + : undefined + const healthyValue = typeof nerStatus.healthy === "boolean" + ? nerStatus.healthy + : undefined + + const isRunning = runningValue + ?? healthyValue + ?? (statusValue ? ["ok", "healthy", "running", "active", "up", "ready"].includes(statusValue) : false) + + return { + isRunning, + statusValue, + device: typeof nerStatus.device === "string" ? nerStatus.device : null, + model: typeof nerStatus.model === "string" ? nerStatus.model : null, + message: typeof nerStatus.message === "string" ? nerStatus.message : null, + } + }, [nerStatus]) const handleRetry = async () => { if (!failedItems?.failed_items || failedItems.failed_items.length === 0) return @@ -143,13 +195,39 @@ export function MonitoringChart({ return () => clearInterval(interval) }, [onRefreshMonitoring]) + useEffect(() => { + const fetchNerStatus = async () => { + if (!token) return + + setIsLoadingNerStatus(true) + setNerStatusError(null) + try { + const api = new NeoApiService() + const statusResponse = await api.getNERStatus(token) + setNerStatus(statusResponse) + } catch (error) { + const message = error instanceof Error + ? error.message + : t("refreshFailed", { ns: "monitoring" }) + setNerStatusError(message) + } finally { + setIsLoadingNerStatus(false) + } + } + + fetchNerStatus() + const interval = window.setInterval(fetchNerStatus, 60000) + return () => window.clearInterval(interval) + }, [token, t]) + return ( - Neo Instance - Data Corpus - Crawling Jobs - Tasks + {t("neoTab", { ns: "monitoring" })} + {t("dataCorpusTab", { ns: "monitoring" })} + {t("crawlingTab", { ns: "monitoring" })} + {t("tasksTab", { ns: "monitoring" })} + {t("nerTab", { ns: "monitoring" })} {/* Neo Tab */} @@ -159,11 +237,6 @@ export function MonitoringChart({ - - {/* Neo Instance Card */} - - Workers + {t("workers", { ns: "monitoring" })} {workers ? (
-
{workers?.total_workers ?? 0}
-

Total workers

-
+
+ {workers?.total_workers ?? 0} +
+

{t("totalWorkers", { ns: "monitoring" })}

+
- Active - {workers?.active_workers ?? 0} + {t("active", { ns: "monitoring" })} + {workers?.active_workers ?? 0}
- Stopping - {workers?.stopping_workers ?? 0} + {t("stale", { ns: "monitoring", defaultValue: "Stale" })} + {staleWorkers}
- Stopped - {workers?.stopped_workers ?? 0} + {t("stopping", { ns: "monitoring" })} + {stoppingWorkers} +
+
+ {t("stopped", { ns: "monitoring" })} + {stoppedWorkers}
) : ( -
No data available
+
{t("noDataAvailable", { ns: "monitoring" })}
)} @@ -233,27 +312,47 @@ export function MonitoringChart({ {/* Work Queue Overview */} - Work Queue + {t("workQueue", { ns: "monitoring" })} - {overview?.work_queue ? ( + {workQueue ? (
-
{overview?.work_queue?.total_items ?? 0}
-

Total items

-
-
- Pending: {overview?.work_queue?.pending_items ?? 0} - Processing: {overview?.work_queue?.processing_items ?? 0} +
+
+
+ {totalItems} +
+

{t("totalItems", { ns: "monitoring" })}

-
- Claimed: {overview?.work_queue?.claimed_items ?? 0} - Failed: {overview?.work_queue?.failed_items ?? 0} +
+
+ {processingItems} +
+

{t("processing", { ns: "monitoring" })}

+
+
+
+
+ {t("pending", { ns: "monitoring" })} + {pendingItems} +
+
+ {t("claimed", { ns: "monitoring" })} + {claimedItems} +
+
+ {t("failed", { ns: "monitoring" })} + {failedItemsCount} +
+
+ {t("abandoned", { ns: "monitoring", defaultValue: "Abandoned" })} + {abandonedItems}
) : ( -
No data available
+
{t("noDataAvailable", { ns: "monitoring" })}
)} @@ -261,27 +360,29 @@ export function MonitoringChart({ {/* Enumeration Status */} - Enumeration + {t("enumeration", { ns: "monitoring" })} {enumeration ? (
-
{enumeration?.completed_enumerations_last_24h ?? 0}
-

Completed (24h)

+
+ {enumeration?.completed_enumerations_last_24h ?? 0} +
+

{t("completed24h", { ns: "monitoring" })}

- Avg Duration: - {enumeration?.avg_enumeration_duration_seconds?.toFixed(1) ?? "0.0"}s + {t("avgDuration", { ns: "monitoring" })}: + {enumeration?.avg_enumeration_duration_seconds?.toFixed(1) ?? "0.0"}s
- Active: - {enumeration?.active_enumerations?.length ?? 0} + {t("active", { ns: "monitoring" })}: + {enumeration?.active_enumerations?.length ?? 0}
) : ( -
No data available
+
{t("noDataAvailable", { ns: "monitoring" })}
)}
@@ -289,32 +390,37 @@ export function MonitoringChart({ {/* Graph Rate Limit */} - Graph Rate Limit + {t("graphRateLimit", { ns: "monitoring" })} {graphRateLimit ? (
-
{graphRateLimit?.requests_remaining ?? 0}
-

Requests remaining

+
+ {graphRateLimit?.requests_remaining ?? 0} +
+

{t("requestsRemaining", { ns: "monitoring" })}

- Made: {graphRateLimit?.requests_made ?? 0} + + {t("made", { ns: "monitoring" })}: + {graphRateLimit?.requests_made ?? 0} + - {graphRateLimit?.rate_limited ? "Limited" : "Active"} + {graphRateLimit?.rate_limited ? t("limited", { ns: "monitoring" }) : t("activeStatus", { ns: "monitoring" })}
{graphRateLimit?.reset_time && (

- Resets: {new Date(graphRateLimit.reset_time).toLocaleTimeString()} + {t("resets", { ns: "monitoring" })}: {new Date(graphRateLimit.reset_time).toLocaleTimeString()}

)}
) : ( -
No data available
+
{t("noDataAvailable", { ns: "monitoring" })}
)}
@@ -322,7 +428,7 @@ export function MonitoringChart({
- Failed Items + {t("failedItems", { ns: "monitoring" })} {failedItems && failedItems.total_failed_items > 0 && ( )}
@@ -341,12 +447,14 @@ export function MonitoringChart({ {failedItems ? (
-
{failedItems?.total_failed_items ?? 0}
-

Total failed

+
+ {failedItems?.total_failed_items ?? 0} +
+

{t("totalFailed", { ns: "monitoring" })}

{failedItems?.failed_items && failedItems.failed_items.length > 0 && (
-

Recent failures:

+

{t("recentFailures", { ns: "monitoring" })}

{failedItems.failed_items.slice(0, 3).map((item, index) => (
{item.filename || item.file_path}
@@ -357,7 +465,7 @@ export function MonitoringChart({ )}
) : ( -
No data available
+
{t("noDataAvailable", { ns: "monitoring" })}
)} @@ -371,6 +479,55 @@ export function MonitoringChart({
+ + {/* NER Tab */} + +
+ + + {t("nerStatus", { ns: "monitoring" })} + + + +
+
+ {isLoadingNerStatus ? ( + + {t("checking", { ns: "monitoring", defaultValue: "Checking..." })} + + ) : nerStatusError ? ( + + {t("notConnected", { ns: "monitoring", defaultValue: "Not connected" })} + + ) : ( + + {normalizedNerStatus?.isRunning + ? t("activeStatus", { ns: "monitoring" }) + : t("unknown", { ns: "monitoring", defaultValue: "Unknown" })} + + )} +
+ {nerStatusError ? ( +

{nerStatusError}

+ ) : normalizedNerStatus?.message ? ( +

{normalizedNerStatus.message}

+ ) : ( +

{t("nerServiceRunning", { ns: "monitoring" })}

+ )} + {normalizedNerStatus?.device && ( +

{t("nerDeviceLabel", { ns: "monitoring" })}: {normalizedNerStatus.device}

+ )} + {normalizedNerStatus?.model && ( +

{t("nerModelLabel", { ns: "monitoring" })}: {normalizedNerStatus.model}

+ )} +
+
+
+
+
) } diff --git a/src/components/data-tables/sharesT.tsx b/src/components/data-tables/sharesT.tsx index 91fac20..38ac711 100644 --- a/src/components/data-tables/sharesT.tsx +++ b/src/components/data-tables/sharesT.tsx @@ -26,6 +26,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table" +import { Button } from "@/components/ui/button" interface SharesTableProps { shares: SharesResponse[] | null @@ -47,6 +48,7 @@ function getStatusIcon(status: string) { case "scheduled": return case "warning": + case "connection_failed": return default: return null @@ -67,6 +69,7 @@ function getStatusBadge(status: string) { pending: "text-yellow-600 border-yellow-200 dark:text-yellow-400 dark:border-yellow-800", scheduled: "text-yellow-600 border-yellow-200 dark:text-yellow-400 dark:border-yellow-800", warning: "text-orange-600 border-orange-200 dark:text-orange-400 dark:border-orange-800", + connection_failed: "text-orange-600 border-orange-200 dark:text-orange-400 dark:border-orange-800", } return ( @@ -82,9 +85,13 @@ function getStatusBadge(status: string) { export function SharesTable({ shares, onShareClick }: SharesTableProps) { const rows = shares ?? [] + const rowsPerPage = 100 + const [currentPage, setCurrentPage] = useState(1) const [columnWidths, setColumnWidths] = useState>({ + source_id: 110, share_path: 300, + protocol: 110, files: 100, username: 150, last_crawled: 200, @@ -155,24 +162,40 @@ export function SharesTable({ shares, onShareClick }: SharesTableProps) { } }, [handleResizeMove, handleResizeEnd]) + useEffect(() => { + const totalPages = Math.max(1, Math.ceil(rows.length / rowsPerPage)) + setCurrentPage((page) => Math.min(page, totalPages)) + }, [rows.length]) + + const totalPages = Math.max(1, Math.ceil(rows.length / rowsPerPage)) + const startIndex = (currentPage - 1) * rowsPerPage + const paginatedRows = rows.slice(startIndex, startIndex + rowsPerPage) + return ( <>
- - Share Path + + Id
handleResizeStart(e, 'share_path')} + onMouseDown={(e) => handleResizeStart(e, 'source_id')} /> - - Files + + Protocol
handleResizeStart(e, 'files')} + onMouseDown={(e) => handleResizeStart(e, 'protocol')} + /> + + + Path +
handleResizeStart(e, 'share_path')} /> @@ -189,6 +212,13 @@ export function SharesTable({ shares, onShareClick }: SharesTableProps) { onMouseDown={(e) => handleResizeStart(e, 'last_crawled')} /> + + Files +
handleResizeStart(e, 'files')} + /> + Status
- {rows.length ? ( - rows.map((share) => ( + {paginatedRows.length ? ( + paginatedRows.map((share) => ( onShareClick(share.id)} className="cursor-pointer hover:bg-muted/50" data-id={share.id} > + {share.id.slice(0, 7)} + {(share.protocol ?? "smb").toUpperCase()} {share.share_path} - {share.last_crawl_file_count} {share.username} {share.last_crawled ? new Date(share.last_crawled).toLocaleString() : "—"} + {share.last_crawl_file_count} {getStatusBadge(share.status)} )) ) : ( - + No shares available. @@ -227,6 +259,31 @@ export function SharesTable({ shares, onShareClick }: SharesTableProps) {
+ {rows.length > 0 ? ( +
+
+ Showing {startIndex + 1}-{Math.min(startIndex + paginatedRows.length, rows.length)} of {rows.length.toLocaleString()} shares · Page {currentPage} of {totalPages} +
+
+ + +
+
+ ) : null} ) } \ No newline at end of file diff --git a/src/components/data-tables/tasksT.tsx b/src/components/data-tables/tasksT.tsx index b093308..c1f7dc3 100644 --- a/src/components/data-tables/tasksT.tsx +++ b/src/components/data-tables/tasksT.tsx @@ -1,4 +1,5 @@ import { useState, useRef, useCallback, useEffect } from "react" +import { useTranslation } from "react-i18next" import { CheckCircle2, XCircle, Clock, Loader2, Ban } from "lucide-react" import type { TasksResponse } from "@/services/neo-api" @@ -11,6 +12,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table" +import { Button } from "@/components/ui/button" interface TasksTableProps { tasks: TasksResponse[] | null @@ -72,7 +74,10 @@ export function formatDuration(startedAt: string | null, completedAt: string | n } export function TasksTable({ tasks, onTaskClick }: TasksTableProps) { + const { t } = useTranslation() const rows = tasks ?? [] + const rowsPerPage = 100 + const [currentPage, setCurrentPage] = useState(1) const [columnWidths, setColumnWidths] = useState>({ name: 250, @@ -146,80 +151,117 @@ export function TasksTable({ tasks, onTaskClick }: TasksTableProps) { } }, [handleResizeMove, handleResizeEnd]) + useEffect(() => { + const totalPages = Math.max(1, Math.ceil(rows.length / rowsPerPage)) + setCurrentPage((page) => Math.min(page, totalPages)) + }, [rows.length]) + + const totalPages = Math.max(1, Math.ceil(rows.length / rowsPerPage)) + const startIndex = (currentPage - 1) * rowsPerPage + const paginatedRows = rows.slice(startIndex, startIndex + rowsPerPage) + return ( -
- - - - - Name -
handleResizeStart(e, 'name')} - /> - - - Share ID -
handleResizeStart(e, 'share_id')} - /> - - - Created -
handleResizeStart(e, 'created_at')} - /> - - - Duration -
handleResizeStart(e, 'duration')} - /> - - - Status -
handleResizeStart(e, 'status')} - /> - - - - - {rows.length ? ( - rows.map((task) => ( - onTaskClick(task)} - className="cursor-pointer hover:bg-muted/50" - > - {task.name} - -
- {task.share_id ? task.share_id : "—"} -
-
- - {new Date(task.created_at).toLocaleString()} - - - {formatDuration(task.started_at, task.completed_at)} - - {getStatusBadge(task.status)} -
- )) - ) : ( + <> +
+
+ - - No tasks available. - + + {t("columnName", { ns: "tasks" })} +
handleResizeStart(e, 'name')} + /> + + + {t("columnShareId", { ns: "tasks" })} +
handleResizeStart(e, 'share_id')} + /> + + + {t("columnCreated", { ns: "tasks" })} +
handleResizeStart(e, 'created_at')} + /> + + + {t("columnDuration", { ns: "tasks" })} +
handleResizeStart(e, 'duration')} + /> + + + {t("columnStatus", { ns: "tasks" })} +
handleResizeStart(e, 'status')} + /> + - )} - -
-
+ + + {paginatedRows.length ? ( + paginatedRows.map((task) => ( + onTaskClick(task)} + className="cursor-pointer hover:bg-muted/50" + > + {task.name} + +
+ {task.share_id ? task.share_id : "—"} +
+
+ + {new Date(task.created_at).toLocaleString()} + + + {formatDuration(task.started_at, task.completed_at)} + + {getStatusBadge(task.status)} +
+ )) + ) : ( + + + {t("noTasksAvailable", { ns: "tasks" })} + + + )} +
+ +
+ + {rows.length > 0 ? ( +
+
+ {t("showingPagination", { ns: "tasks", from: startIndex + 1, to: Math.min(startIndex + paginatedRows.length, rows.length), total: rows.length.toLocaleString(), page: currentPage, pages: totalPages })} +
+
+ + +
+
+ ) : null} + ) } \ No newline at end of file diff --git a/src/components/data-tables/usersT.tsx b/src/components/data-tables/usersT.tsx index f1a21df..2a079f2 100644 --- a/src/components/data-tables/usersT.tsx +++ b/src/components/data-tables/usersT.tsx @@ -1,6 +1,8 @@ // Copyright 2025 NetApp, Inc. All Rights Reserved. "use client" +import { useEffect, useState } from "react" + import { IconPasswordUser } from "@tabler/icons-react" @@ -27,66 +29,107 @@ interface UsersTableProps { users?: UserResponse[] | null me?: MeResponse | null onRequestPasswordChange?: () => void + onLinkEntra?: () => Promise + onUnlinkEntra?: () => Promise } export function UsersTable({ users, me, onRequestPasswordChange }: UsersTableProps) { const rows = users ?? [] + const rowsPerPage = 100 + const [currentPage, setCurrentPage] = useState(1) + + useEffect(() => { + const totalPages = Math.max(1, Math.ceil(rows.length / rowsPerPage)) + setCurrentPage((page) => Math.min(page, totalPages)) + }, [rows.length]) + + const totalPages = Math.max(1, Math.ceil(rows.length / rowsPerPage)) + const startIndex = (currentPage - 1) * rowsPerPage + const paginatedRows = rows.slice(startIndex, startIndex + rowsPerPage) return ( -
- - - - Username - Email - Active - Admin - Created - Last Login - Actions - - - - {rows.length ? ( - rows.map((user) => { - const isCurrent = me?.id === user.id - return ( - - - {user.username} - {isCurrent ? (current) : null} - - {user.email ?? "-"} - {user.is_active ? "Yes" : "No"} - {user.is_admin ? "Yes" : "No"} - {new Date(user.created_at).toLocaleString()} - - {user.last_login ? new Date(user.last_login).toLocaleString() : "—"} - - - {isCurrent && onRequestPasswordChange ? ( - - ) : null} - - - ) - }) - ) : ( + <> +
+
+ - - No users available. - + Username + Email + Active + Admin + Created + Last Login + Actions - )} - -
-
+ + + {paginatedRows.length ? ( + paginatedRows.map((user) => { + const isCurrent = me?.id === user.id + return ( + + + {user.username} + {isCurrent ? (current) : null} + + {user.email ?? "-"} + {user.is_active ? "Yes" : "No"} + {user.is_admin ? "Yes" : "No"} + {new Date(user.created_at).toLocaleString()} + + {user.last_login ? new Date(user.last_login).toLocaleString() : "—"} + + + {isCurrent && onRequestPasswordChange ? ( + + ) : null} + + + ) + }) + ) : ( + + + No users available. + + + )} + + +
+ + {rows.length > 0 ? ( +
+
+ Showing {startIndex + 1}-{Math.min(startIndex + paginatedRows.length, rows.length)} of {rows.length.toLocaleString()} users · Page {currentPage} of {totalPages} +
+
+ + +
+
+ ) : null} + ) } \ No newline at end of file diff --git a/src/components/dialogs/add-to-dataset-dialog.tsx b/src/components/dialogs/add-to-dataset-dialog.tsx new file mode 100644 index 0000000..f140d9c --- /dev/null +++ b/src/components/dialogs/add-to-dataset-dialog.tsx @@ -0,0 +1,138 @@ +// Copyright 2025 NetApp, Inc. All Rights Reserved. +"use client" + +import { useState } from "react" +import { IconDatabasePlus } from "@tabler/icons-react" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import type { Dataset } from "@/services/models" + +interface AddToDatasetDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + datasets: Dataset[] + fileCount: number + onAdd: (datasetId: string, notes?: string) => Promise +} + +export function AddToDatasetDialog({ + open, + onOpenChange, + datasets, + fileCount, + onAdd, +}: AddToDatasetDialogProps) { + const [selectedDatasetId, setSelectedDatasetId] = useState("") + const [notes, setNotes] = useState("") + const [isAdding, setIsAdding] = useState(false) + + const handleAdd = async () => { + if (!selectedDatasetId) return + + setIsAdding(true) + try { + await onAdd(selectedDatasetId, notes.trim() || undefined) + onOpenChange(false) + // Reset state + setSelectedDatasetId("") + setNotes("") + } finally { + setIsAdding(false) + } + } + + const handleOpenChange = (open: boolean) => { + if (!open) { + setSelectedDatasetId("") + setNotes("") + } + onOpenChange(open) + } + + return ( + + + + + + Add to Dataset + + + Add {fileCount} file{fileCount !== 1 ? "s" : ""} to an existing dataset. + + + +
+
+ + {datasets.length === 0 ? ( +

+ No datasets available. Create a dataset first. +

+ ) : ( + + )} +
+ +
+ +