Compare commits
4 Commits
backup/ref
...
v1.10.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 6760ab0abf | |||
| d9e592cc97 | |||
| 062df8cfd9 | |||
| 074d4f1f8d |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -94,4 +94,12 @@ aInventory-PROD*.zip
|
|||||||
*.crt
|
*.crt
|
||||||
*.cert
|
*.cert
|
||||||
|
|
||||||
__push_ALL_to_remote.sh
|
|
||||||
|
# ── Local AI Tooling & Persistent Paths ──────────────────────
|
||||||
|
.tool_paths
|
||||||
|
.git_path
|
||||||
|
|
||||||
|
# ── Temporary Docker / Verification Artifacts ────────────────
|
||||||
|
.docker_tmp/
|
||||||
|
.tmp_docker/
|
||||||
|
scratch/.npm/
|
||||||
|
|||||||
@@ -112,5 +112,9 @@ To ensure deployment stability on macOS environments with potentially broken dev
|
|||||||
109: - **Unified AI Core:** The backend uses an abstraction layer to handle multiple AI providers (Gemini and Claude).
|
109: - **Unified AI Core:** The backend uses an abstraction layer to handle multiple AI providers (Gemini and Claude).
|
||||||
110: - **Dynamic Configuration:** System settings (AI Provider, API Keys, Backup Policies) are managed via `backend/config_manager.py`, allowing real-time updates without restarting the container.
|
110: - **Dynamic Configuration:** System settings (AI Provider, API Keys, Backup Policies) are managed via `backend/config_manager.py`, allowing real-time updates without restarting the container.
|
||||||
111: - **Admin Standardization:** The Admin Dashboard features a standardized configuration UI with secure field masking for sensitive credentials.
|
111: - **Admin Standardization:** The Admin Dashboard features a standardized configuration UI with secure field masking for sensitive credentials.
|
||||||
- **Architectural Modularization (v1.10.0):** The monolithic Admin and Inventory pages have been refactored into a modular architecture. Frontend logic is decoupled into custom React hooks (`useAdmin`), and UI is extracted into standalone components (`IdentityManager`, `DatabaseManager`, etc.). Backend admin endpoints are split into domain-specific routers (`backups`, `config`) for improved maintainability.
|
- **Architectural Modularization (v1.10.0):**
|
||||||
|
- **Frontend:** The monolithic Admin Dashboard has been decomposed into domain-specific components: `IdentityManager`, `DatabaseManager`, `LdapManager`, `AiManager`, and `CategoryManager`.
|
||||||
|
- **Logic:** Business logic is centralized in the `useAdmin` custom React hook, ensuring clean separation of concerns.
|
||||||
|
- **Backend:** Administrative endpoints are split into the `backend/routers/admin/` package, with specialized routers for `backups` and `config`.
|
||||||
|
- **Stability:** Docker builds are secured against lockfile mismatches by enforcing strict dependency synchronization.
|
||||||
- **Verification Infrastructure:** A dual-layer testing suite is implemented: Pytest for backend integration (using in-memory SQLite and mocked auth) and Vitest for frontend logic validation.
|
- **Verification Infrastructure:** A dual-layer testing suite is implemented: Pytest for backend integration (using in-memory SQLite and mocked auth) and Vitest for frontend logic validation.
|
||||||
|
|||||||
@@ -49,11 +49,12 @@ To generate a clean production package and snapshot the current state:
|
|||||||
- Accessible markup with `role="status"` and `aria-hidden` attributes
|
- Accessible markup with `role="status"` and `aria-hidden` attributes
|
||||||
- **Pages Updated:** Inventory (Categories, Item Types, Total Boxes), Logs (Total Events, Check in/out), Admin (Local Archives)
|
- **Pages Updated:** Inventory (Categories, Item Types, Total Boxes), Logs (Total Events, Check in/out), Admin (Local Archives)
|
||||||
- **Tested on:** iPhone SE (375px), iPhone 12 (390px), iPhone 14 Pro Max (430px), tablets (768px), desktop (1024px)
|
- **Tested on:** iPhone SE (375px), iPhone 12 (390px), iPhone 14 Pro Max (430px), tablets (768px), desktop (1024px)
|
||||||
- **Multi-AI Selection & Secure Config (v1.9.23):**
|
|
||||||
- Support for both **Google Gemini** and **Anthropic Claude** for AI label extraction.
|
|
||||||
- Dynamically configurable AI providers via the Admin Dashboard.
|
|
||||||
- Centralized `ConfigManager` for secure API key and environment management.
|
|
||||||
- **Admin UI Standardization:** Refactored Admin page with unified StatCards and secure input masking for system credentials.
|
- **Admin UI Standardization:** Refactored Admin page with unified StatCards and secure input masking for system credentials.
|
||||||
|
- **Modular Admin Architecture (v1.10.0):**
|
||||||
|
- Decomposed monolithic pages into domain-specific components: `IdentityManager`, `DatabaseManager`, `LdapManager`, `AiManager`, and `CategoryManager`.
|
||||||
|
- Centralized state logic into a custom `useAdmin` hook for improved maintainability.
|
||||||
|
- Split backend admin endpoints into specific routers (`backups`, `config`) for better scalability.
|
||||||
|
- Optimized Docker configuration with persistent binary paths and fixed log-piping via `su-exec`.
|
||||||
|
|
||||||
## 🏗 Technical Overview
|
## 🏗 Technical Overview
|
||||||
* **Backend:** FastAPI (Python 3.12+)
|
* **Backend:** FastAPI (Python 3.12+)
|
||||||
|
|||||||
4
__push_ALL_to_remote.sh
Executable file
4
__push_ALL_to_remote.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
git push origin --all
|
||||||
|
git push origin --tags
|
||||||
@@ -35,7 +35,5 @@ RUN chmod +x /app/scripts/init_data.sh /app/backend/entrypoint.sh
|
|||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
EXPOSE 8000
|
|
||||||
|
|
||||||
# Entrypoint runs init_data.sh first, then starts uvicorn
|
# Entrypoint runs init_data.sh first, then starts uvicorn
|
||||||
ENTRYPOINT ["/app/backend/entrypoint.sh"]
|
ENTRYPOINT ["/app/backend/entrypoint.sh"]
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ def load_config():
|
|||||||
load_dotenv(backend_env_path)
|
load_dotenv(backend_env_path)
|
||||||
log.info(f"ℹ️ Loaded local configuration from {backend_env_path}")
|
log.info(f"ℹ️ Loaded local configuration from {backend_env_path}")
|
||||||
else:
|
else:
|
||||||
log.warning("⚠️ No .env config files found. Relying on system environment variables.")
|
log.info("ℹ️ [CONFIG] Using system environment variables (Docker/Server environment).")
|
||||||
|
|
||||||
# Auto-run if imported
|
# Auto-run if imported
|
||||||
load_config()
|
load_config()
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ bash /app/scripts/init_data.sh
|
|||||||
# Fix permissions for mounted volumes (which might be root-owned by the host)
|
# Fix permissions for mounted volumes (which might be root-owned by the host)
|
||||||
echo "🐳 [Docker] Fixing volume permissions..."
|
echo "🐳 [Docker] Fixing volume permissions..."
|
||||||
chown -R appuser:appuser "${DATA_DIR}" "${LOGS_DIR}"
|
chown -R appuser:appuser "${DATA_DIR}" "${LOGS_DIR}"
|
||||||
|
chmod -R 770 "${DATA_DIR}" "${LOGS_DIR}"
|
||||||
|
|
||||||
|
# Verify logic: Ensure appuser can actually write before we hand off
|
||||||
|
echo "🐳 [Docker] Verifying directory writability..."
|
||||||
|
gosu appuser touch "${LOGS_DIR}/.startup_test" && rm "${LOGS_DIR}/.startup_test"
|
||||||
|
gosu appuser touch "${DATA_DIR}/.startup_test" && rm "${DATA_DIR}/.startup_test"
|
||||||
|
|
||||||
# Hand off to the application server as non-root user
|
# Hand off to the application server as non-root user
|
||||||
echo "🐳 [Docker] Starting uvicorn as appuser..."
|
echo "🐳 [Docker] Starting uvicorn as appuser..."
|
||||||
|
|||||||
@@ -27,26 +27,25 @@ def setup_logger():
|
|||||||
datefmt="%Y-%m-%d %H:%M:%S"
|
datefmt="%Y-%m-%d %H:%M:%S"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Console Handler
|
|
||||||
console_handler = logging.StreamHandler()
|
|
||||||
console_handler.setFormatter(formatter)
|
|
||||||
|
|
||||||
# File Handler (10MB max, keep 5 backups)
|
# File Handler (10MB max, keep 5 backups)
|
||||||
|
try:
|
||||||
file_handler = RotatingFileHandler(
|
file_handler = RotatingFileHandler(
|
||||||
LOG_FILE_PATH, maxBytes=10*1024*1024, backupCount=5
|
LOG_FILE_PATH, maxBytes=10*1024*1024, backupCount=5
|
||||||
)
|
)
|
||||||
file_handler.setFormatter(formatter)
|
file_handler.setFormatter(formatter)
|
||||||
|
|
||||||
# Attach handlers
|
|
||||||
logger.addHandler(console_handler)
|
|
||||||
logger.addHandler(file_handler)
|
logger.addHandler(file_handler)
|
||||||
|
|
||||||
# Special redirect for uvicorn logs to also go to file
|
# Special redirect for uvicorn logs to also go to file
|
||||||
uvicorn_logger = logging.getLogger("uvicorn")
|
logging.getLogger("uvicorn").addHandler(file_handler)
|
||||||
uvicorn_logger.addHandler(file_handler)
|
logging.getLogger("uvicorn.access").addHandler(file_handler)
|
||||||
|
except (PermissionError, IOError) as e:
|
||||||
|
logger.warning(f"⚠️ [LOGGING] Cannot write to log file at {LOG_FILE_PATH}: {e}")
|
||||||
|
logger.warning("⚠️ [LOGGING] Continuing with console-only output.")
|
||||||
|
|
||||||
uvicorn_access_logger = logging.getLogger("uvicorn.access")
|
# Console Handler (always registered)
|
||||||
uvicorn_access_logger.addHandler(file_handler)
|
console_handler = logging.StreamHandler()
|
||||||
|
console_handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(console_handler)
|
||||||
|
|
||||||
return logger
|
return logger
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "1.10.0",
|
"version": "1.10.2",
|
||||||
"last_build": "2026-04-15-1645",
|
"last_build": "2026-04-15-1754",
|
||||||
"codename": "ModularAdmin",
|
"codename": "ModularAdmin",
|
||||||
"commit": "unknown"
|
"commit": "d9e592cc"
|
||||||
}
|
}
|
||||||
@@ -26,5 +26,9 @@ EOF
|
|||||||
chown nextjs:nodejs /app/public/network.json
|
chown nextjs:nodejs /app/public/network.json
|
||||||
|
|
||||||
# Hand off to the application server as the nextjs user
|
# Hand off to the application server as the nextjs user
|
||||||
echo "🐳 [Docker] Starting Next.js standalone server as nextjs user..."
|
echo "🐳 [Docker] Starting Next.js server..."
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
exec su-exec nextjs node server.js
|
exec su-exec nextjs node server.js
|
||||||
|
else
|
||||||
|
exec su-exec nextjs "$@"
|
||||||
|
fi
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint"
|
||||||
"test": "vitest"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.15.0",
|
"axios": "^1.15.0",
|
||||||
@@ -28,15 +27,10 @@
|
|||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@testing-library/jest-dom": "^6.5.0",
|
|
||||||
"@testing-library/react": "^16.0.1",
|
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.0.0",
|
"eslint-config-next": "15.0.0",
|
||||||
"jsdom": "^25.0.1",
|
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5",
|
"typescript": "^5"
|
||||||
"vitest": "^2.1.2",
|
|
||||||
"@vitejs/plugin-react": "^4.3.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import { renderHook, waitFor } from '@testing-library/react';
|
|
||||||
import { useAdmin } from '@/hooks/useAdmin';
|
|
||||||
import { inventoryApi } from '@/lib/api';
|
|
||||||
import { vi, describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
// Mock the API
|
|
||||||
vi.mock('@/lib/api', () => ({
|
|
||||||
inventoryApi: {
|
|
||||||
getUsers: vi.fn(),
|
|
||||||
getCategories: vi.fn(),
|
|
||||||
getLdapConfig: vi.fn(),
|
|
||||||
getDbBackups: vi.fn(),
|
|
||||||
getDbStats: vi.fn(),
|
|
||||||
getDbSettings: vi.fn(),
|
|
||||||
getAiPrompt: vi.fn(),
|
|
||||||
getAiConfig: vi.fn(),
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock toast
|
|
||||||
vi.mock('react-hot-toast', () => ({
|
|
||||||
toast: {
|
|
||||||
error: vi.fn(),
|
|
||||||
success: vi.fn(),
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('useAdmin hook', () => {
|
|
||||||
it('should initialize with loading state and fetch data', async () => {
|
|
||||||
(inventoryApi.getUsers as any).mockResolvedValue([{ id: 1, username: 'testuser' }]);
|
|
||||||
(inventoryApi.getCategories as any).mockResolvedValue([]);
|
|
||||||
(inventoryApi.getLdapConfig as any).mockResolvedValue({ server_uri: 'ldap://fake' });
|
|
||||||
(inventoryApi.getDbBackups as any).mockResolvedValue([]);
|
|
||||||
(inventoryApi.getDbStats as any).mockResolvedValue({ backup_count: 0 });
|
|
||||||
(inventoryApi.getDbSettings as any).mockResolvedValue({ retention_count: 5 });
|
|
||||||
(inventoryApi.getAiPrompt as any).mockResolvedValue({ value: 'test prompt' });
|
|
||||||
(inventoryApi.getAiConfig as any).mockResolvedValue({ active_provider: 'gemini' });
|
|
||||||
|
|
||||||
const { result } = renderHook(() => useAdmin());
|
|
||||||
|
|
||||||
expect(result.current.loading).toBe(true);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(result.current.loading).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.current.users).toHaveLength(1);
|
|
||||||
expect(result.current.users[0].username).toBe('testuser');
|
|
||||||
expect(result.current.aiPrompt).toBe('test prompt');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { defineConfig } from 'vitest/config';
|
|
||||||
import react from '@vitejs/plugin-react';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [react()],
|
|
||||||
test: {
|
|
||||||
environment: 'jsdom',
|
|
||||||
globals: true,
|
|
||||||
setupFiles: ['./vitest.setup.ts'],
|
|
||||||
alias: {
|
|
||||||
'@': path.resolve(__dirname, './'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import '@testing-library/jest-dom';
|
|
||||||
Reference in New Issue
Block a user