Consolidate Reports and Data Entry apps into a single Vue.js SPA with: Architecture: - Module-based structure with lazy-loaded routes (@reports, @data-entry) - Error boundaries per module to prevent cascade failures - Dual API proxy in Vite for microservices (reports:8001, data-entry:8003) - Pinia store factories for shared auth, company, and period stores - Vite path aliases for clear module boundaries (@shared, @reports, @data-entry) Service Management: - Granular service control scripts (backend-reports.sh, backend-data-entry.sh, bot.sh, frontend.sh) - 87% faster frontend restart: 7s vs 53s full restart - 38% faster full startup: 33s vs 53s via parallel backend initialization - Enhanced start-dev.sh with proper service timeouts (OCR: 30s, Vite: 15s, Bot: 10s) - status.sh for comprehensive health checks Features: - Auto-select first company on login with period auto-load - Hamburger menu with feature toggle support - JWT token auto-injection via axios interceptors - Unified header with company/period selectors - IIS web.config for production deployment with multi-API routing UX Improvements: - Vue watchers for reactive company/period loading - Lazy store initialization with graceful error handling - Period persistence per user+company in localStorage - Feature flags for optional modules Deployment: - Single IIS site serves unified frontend with API proxy rules - Maintains separate backend processes for microservices - Windows line ending fixes (.env CRLF → LF conversion) Stats: 112 files changed, 38,342 insertions(+), 2,342 deletions(-) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
141 lines
10 KiB
JSON
141 lines
10 KiB
JSON
{
|
|
"patterns": [
|
|
{
|
|
"id": "pat_20251222_182000",
|
|
"timestamp": "2025-12-22T18:20:00Z",
|
|
"title": "Unified Vue SPA with Module Isolation via Error Boundaries",
|
|
"description": "Consolidate multiple Vue apps into a single SPA using lazy-loaded modules wrapped in error boundaries. Each module has its own layout component with ErrorBoundary wrapper to prevent crashes from propagating across modules.",
|
|
"context": "Implemented while unifying Reports App and Data Entry App into single deployment",
|
|
"example": {
|
|
"file": "src/modules/reports/ReportsLayout.vue",
|
|
"lines": "1-7",
|
|
"snippet": "<template>\n <ErrorBoundary module-name=\"Rapoarte\">\n <router-view />\n </ErrorBoundary>\n</template>\n\n<script setup>\nimport ErrorBoundary from '@shared/components/ErrorBoundary.vue'\n</script>"
|
|
},
|
|
"tags": ["vue", "spa", "error-boundary", "module-isolation", "architecture"],
|
|
"feature": "unified-app",
|
|
"usageCount": 0
|
|
},
|
|
{
|
|
"id": "pat_20251222_182001",
|
|
"timestamp": "2025-12-22T18:20:01Z",
|
|
"title": "Dual API Proxy Pattern in Vite for Microservices",
|
|
"description": "Configure Vite dev server to proxy multiple backend microservices under different paths. Allows unified frontend to communicate with separate backend services while maintaining CORS and authentication.",
|
|
"context": "Needed to route /api/reports to port 8001 and /api/data-entry to port 8003 from single frontend",
|
|
"example": {
|
|
"file": "vite.config.js",
|
|
"lines": "38-62",
|
|
"snippet": "proxy: {\n '/api/reports': {\n target: 'http://localhost:8001',\n changeOrigin: true,\n rewrite: (path) => path.replace(/^\\/api\\/reports/, '/api'),\n configure: (proxy) => {\n proxy.on('proxyReq', (proxyReq, req) => {\n if (req.headers.authorization) {\n proxyReq.setHeader('Authorization', req.headers.authorization);\n }\n });\n }\n },\n '/api/data-entry': {\n target: 'http://localhost:8003',\n changeOrigin: true,\n rewrite: (path) => path.replace(/^\\/api\\/data-entry/, '/api')\n }\n}"
|
|
},
|
|
"tags": ["vite", "proxy", "microservices", "api", "configuration"],
|
|
"feature": "unified-app",
|
|
"usageCount": 0
|
|
},
|
|
{
|
|
"id": "pat_20251222_182002",
|
|
"timestamp": "2025-12-22T18:20:02Z",
|
|
"title": "Pinia Store Factory Pattern for Shared Stores",
|
|
"description": "Create shared Pinia stores as factory functions that accept API service instances. Each module instantiates the shared stores with its own API service, ensuring proper module isolation while sharing store logic.",
|
|
"context": "Auth, companies, and accounting period stores needed to work with both Reports (port 8001) and Data Entry (port 8003) APIs",
|
|
"example": {
|
|
"file": "src/shared/stores/auth.js",
|
|
"lines": "21-32",
|
|
"snippet": "export function createAuthStore(apiService) {\n return defineStore('auth', () => {\n const accessToken = ref(localStorage.getItem('access_token'))\n // ... state\n\n const login = async (credentials) => {\n const response = await apiService.post('/auth/login', credentials)\n // ... handle response\n }\n\n return { login, logout, isAuthenticated, currentUser }\n })\n}"
|
|
},
|
|
"tags": ["pinia", "stores", "factory-pattern", "module-isolation", "vue"],
|
|
"feature": "unified-app",
|
|
"usageCount": 0
|
|
},
|
|
{
|
|
"id": "pat_20251222_182003",
|
|
"timestamp": "2025-12-22T18:20:03Z",
|
|
"title": "Module-Specific Shared Store Instances",
|
|
"description": "Instantiate shared store factories in each module's dedicated file to ensure proper API service binding. Prevents import confusion and ensures each module uses its own API base URL.",
|
|
"context": "Needed to prevent modules from accidentally using wrong API service when importing shared stores",
|
|
"example": {
|
|
"file": "src/modules/reports/stores/sharedStores.js",
|
|
"lines": "1-18",
|
|
"snippet": "import { createAuthStore } from '@shared/stores/auth'\nimport { createCompaniesStore } from '@shared/stores/companies'\nimport { createAccountingPeriodStore } from '@shared/stores/accountingPeriod'\nimport api from '@reports/services/api'\n\n// Create instances with Reports API service\nexport const useAuthStore = createAuthStore(api)\nexport const useCompanyStore = createCompaniesStore(api, useAuthStore)\nexport const useAccountingPeriodStore = createAccountingPeriodStore(api)\n\n// All reports components import from this file, not directly from @shared"
|
|
},
|
|
"tags": ["pinia", "stores", "module-isolation", "api", "architecture"],
|
|
"feature": "unified-app",
|
|
"usageCount": 0
|
|
},
|
|
{
|
|
"id": "pat_20251222_182004",
|
|
"timestamp": "2025-12-22T18:20:04Z",
|
|
"title": "Vite Alias Strategy for Module Organization",
|
|
"description": "Use Vite path aliases to create clear module boundaries: @shared for shared code, @reports and @data-entry for module-specific code. Makes imports explicit and prevents accidental cross-module dependencies.",
|
|
"context": "Needed clear import paths when consolidating two apps with different import patterns",
|
|
"example": {
|
|
"file": "vite.config.js",
|
|
"lines": "19-26",
|
|
"snippet": "resolve: {\n alias: {\n '@': fileURLToPath(new URL('./src', import.meta.url)),\n '@shared': fileURLToPath(new URL('./src/shared', import.meta.url)),\n '@reports': fileURLToPath(new URL('./src/modules/reports', import.meta.url)),\n '@data-entry': fileURLToPath(new URL('./src/modules/data-entry', import.meta.url))\n },\n dedupe: ['vue', 'vue-router', 'pinia', 'primevue']\n}"
|
|
},
|
|
"tags": ["vite", "aliases", "imports", "module-organization", "architecture"],
|
|
"feature": "unified-app",
|
|
"usageCount": 0
|
|
},
|
|
{
|
|
"id": "pat_20251222_182005",
|
|
"timestamp": "2025-12-22T18:20:05Z",
|
|
"title": "IIS URL Rewrite Rules for SPA with Multiple API Backends",
|
|
"description": "Configure IIS web.config to proxy different API paths to different backend ports while serving SPA for all other routes. Enables single IIS site to route to multiple microservices.",
|
|
"context": "Production deployment needs unified frontend to communicate with both backend services through IIS",
|
|
"example": {
|
|
"file": "public/web.config",
|
|
"lines": "5-28",
|
|
"snippet": "<rewrite>\n <rules>\n <rule name=\"Proxy Reports API\" stopProcessing=\"true\">\n <match url=\"^api/reports/(.*)\" />\n <action type=\"Rewrite\" url=\"http://localhost:8001/api/{R:1}\" />\n </rule>\n <rule name=\"Proxy Data Entry API\" stopProcessing=\"true\">\n <match url=\"^api/data-entry/(.*)\" />\n <action type=\"Rewrite\" url=\"http://localhost:8003/api/{R:1}\" />\n </rule>\n <rule name=\"SPA Fallback\" stopProcessing=\"true\">\n <match url=\".*\" />\n <conditions logicalGrouping=\"MatchAll\">\n <add input=\"{REQUEST_FILENAME}\" matchType=\"IsFile\" negate=\"true\" />\n </conditions>\n <action type=\"Rewrite\" url=\"/index.html\" />\n </rule>\n </rules>\n</rewrite>"
|
|
},
|
|
"tags": ["iis", "deployment", "spa", "microservices", "proxy"],
|
|
"feature": "unified-app",
|
|
"usageCount": 0
|
|
},
|
|
{
|
|
"id": "pat_20251224_001000",
|
|
"timestamp": "2025-12-24T00:10:00Z",
|
|
"title": "Vue Watcher for Auto-Loading Dependent Data",
|
|
"description": "Use Vue watch() to automatically trigger data loading when dependent selections change. Watch company selection changes to auto-load accounting periods, ensuring UI stays synchronized without manual intervention.",
|
|
"context": "Users manually selected company but period dropdown stayed on placeholder instead of auto-selecting current period",
|
|
"example": {
|
|
"file": "src/App.vue",
|
|
"lines": "88-100",
|
|
"snippet": "watch(\n () => companyStore.selectedCompany,\n async (newCompany, oldCompany) => {\n if (newCompany && newCompany.id_firma && newCompany !== oldCompany) {\n console.log('[App] Company changed via watch, loading periods for:', newCompany.id_firma)\n await periodStore.loadPeriods(newCompany.id_firma)\n console.log('[App] Periods auto-loaded successfully')\n }\n },\n { immediate: true }\n)"
|
|
},
|
|
"tags": ["vue", "watch", "reactive", "auto-load", "ux"],
|
|
"feature": "unified-app-ux",
|
|
"usageCount": 0
|
|
},
|
|
{
|
|
"id": "pat_20251224_001001",
|
|
"timestamp": "2025-12-24T00:10:01Z",
|
|
"title": "Axios Request Interceptor for JWT Token Injection",
|
|
"description": "Add axios request interceptor to automatically inject JWT Bearer token from localStorage into all API requests. Eliminates manual token handling in every API call and prevents 401/500 authentication errors.",
|
|
"context": "API requests were failing with 500 errors because JWT token wasn't being sent in Authorization header",
|
|
"example": {
|
|
"file": "src/App.vue",
|
|
"lines": "61-68",
|
|
"snippet": "authApi.interceptors.request.use(config => {\n const token = localStorage.getItem('access_token')\n if (token) {\n config.headers.Authorization = `Bearer ${token}`\n }\n return config\n})"
|
|
},
|
|
"tags": ["axios", "jwt", "authentication", "interceptor", "api"],
|
|
"feature": "unified-app-ux",
|
|
"usageCount": 0
|
|
},
|
|
{
|
|
"id": "pat_20251224_001002",
|
|
"timestamp": "2025-12-24T00:10:02Z",
|
|
"title": "Pinia Store Factory with Lazy Instantiation",
|
|
"description": "When store factories need to access other stores, use lazy instantiation with try-catch to avoid timing issues. Access stores inside functions (not at module level) and gracefully handle cases where stores aren't ready yet.",
|
|
"context": "Period store tried to access auth/company stores for localStorage key generation but got 'useAuthStore is not a function' error",
|
|
"example": {
|
|
"file": "src/shared/stores/accountingPeriod.js",
|
|
"lines": "52-64",
|
|
"snippet": "const getStorageKey = () => {\n try {\n const authStore = useAuthStore();\n const companyStore = useCompanyStore();\n const username = authStore.user?.username;\n const companyId = companyStore.selectedCompany?.id_firma;\n if (!username || !companyId) return null;\n return `selected_period_${username}_${companyId}`;\n } catch (e) {\n // Stores not yet initialized, skip localStorage\n return null;\n }\n};"
|
|
},
|
|
"tags": ["pinia", "stores", "lazy-initialization", "try-catch", "timing"],
|
|
"feature": "unified-app-ux",
|
|
"usageCount": 0
|
|
}
|
|
],
|
|
"updated": "2025-12-24T00:10:00Z"
|
|
}
|