Update and syc dev branch #3
38
.atl/skill-registry.md
Normal file
38
.atl/skill-registry.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Skill Registry — OmniOilPersonal
|
||||||
|
|
||||||
|
Generated: 2026-04-02
|
||||||
|
|
||||||
|
## Active Skills (by context)
|
||||||
|
|
||||||
|
| Context | Skill | Path |
|
||||||
|
|---------|-------|------|
|
||||||
|
| React components, hooks, JSX | react-19 | `~/.claude/skills/react-19/SKILL.md` |
|
||||||
|
| TypeScript types, interfaces | typescript | `~/.claude/skills/typescript/SKILL.md` |
|
||||||
|
| Tailwind classes, styling | tailwind-4 | `~/.claude/skills/tailwind-4/SKILL.md` |
|
||||||
|
| Zod schemas, validation | zod-4 | `~/.claude/skills/zod-4/SKILL.md` |
|
||||||
|
| Zustand stores, state | zustand-5 | `~/.claude/skills/zustand-5/SKILL.md` |
|
||||||
|
| Next.js, app router | nextjs-15 | `~/.claude/skills/nextjs-15/SKILL.md` |
|
||||||
|
| AI SDK, streaming | ai-sdk-5 | `~/.claude/skills/ai-sdk-5/SKILL.md` |
|
||||||
|
| Playwright E2E tests | playwright | `~/.claude/skills/playwright/SKILL.md` |
|
||||||
|
| Pytest, Python testing | pytest | `~/.claude/skills/pytest/SKILL.md` |
|
||||||
|
| Django, DRF, REST API | django-drf | `~/.claude/skills/django-drf/SKILL.md` |
|
||||||
|
| Go testing, Bubbletea | go-testing | `~/.claude/skills/go-testing/SKILL.md` |
|
||||||
|
|
||||||
|
## SDD Skills
|
||||||
|
|
||||||
|
| Skill | Path |
|
||||||
|
|-------|------|
|
||||||
|
| sdd-init | `~/.claude/skills/sdd-init/SKILL.md` |
|
||||||
|
| sdd-explore | `~/.claude/skills/sdd-explore/SKILL.md` |
|
||||||
|
| sdd-propose | `~/.claude/skills/sdd-propose/SKILL.md` |
|
||||||
|
| sdd-spec | `~/.claude/skills/sdd-spec/SKILL.md` |
|
||||||
|
| sdd-design | `~/.claude/skills/sdd-design/SKILL.md` |
|
||||||
|
| sdd-tasks | `~/.claude/skills/sdd-tasks/SKILL.md` |
|
||||||
|
| sdd-apply | `~/.claude/skills/sdd-apply/SKILL.md` |
|
||||||
|
| sdd-verify | `~/.claude/skills/sdd-verify/SKILL.md` |
|
||||||
|
| sdd-archive | `~/.claude/skills/sdd-archive/SKILL.md` |
|
||||||
|
|
||||||
|
## Project Conventions
|
||||||
|
|
||||||
|
- Global CLAUDE.md: `~/.claude/CLAUDE.md`
|
||||||
|
- No project-level CLAUDE.md found
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="es">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/png" href="/pwa-192.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>frontend</title>
|
<meta name="theme-color" content="#06b6d4" />
|
||||||
|
<title>OmniOil</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
4277
services/frontend/package-lock.json
generated
4277
services/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@
|
|||||||
"@types/node": "^25.5.0",
|
"@types/node": "^25.5.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"dexie": "^4.4.2",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"vite": "^7.2.4"
|
"vite": "^7.2.4",
|
||||||
|
"vite-plugin-pwa": "^1.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
services/frontend/public/pwa-192.png
Executable file
BIN
services/frontend/public/pwa-192.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
services/frontend/public/pwa-512.png
Executable file
BIN
services/frontend/public/pwa-512.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
67
services/frontend/src/app/layouts/FieldLayout.tsx
Normal file
67
services/frontend/src/app/layouts/FieldLayout.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { Outlet, NavLink } from 'react-router-dom'
|
||||||
|
import { Home, FlaskConical, ArrowLeftRight, CircleOff, RefreshCw, Droplets } from 'lucide-react'
|
||||||
|
import { useSyncStore } from '@/lib/sync/sync-store'
|
||||||
|
import { useNetworkStatus } from '@/lib/sync/use-network-status'
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ to: '/app/field', label: 'Inicio', icon: Home, end: true },
|
||||||
|
{ to: '/app/field/measurements', label: 'Medición', icon: FlaskConical, end: false },
|
||||||
|
{ to: '/app/field/movements', label: 'Movimientos', icon: ArrowLeftRight, end: false },
|
||||||
|
{ to: '/app/field/well-shutdowns', label: 'Paradas', icon: CircleOff, end: false },
|
||||||
|
{ to: '/app/field/sync', label: 'Sync', icon: RefreshCw, end: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function FieldLayout() {
|
||||||
|
useNetworkStatus()
|
||||||
|
|
||||||
|
const isOnline = useSyncStore((s) => s.isOnline)
|
||||||
|
const pendingCount = useSyncStore((s) => s.pendingCount)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-screen bg-[var(--background)]">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="flex items-center justify-between h-14 px-4 border-b border-[var(--border)] bg-[var(--card)] shrink-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Droplets className="h-5 w-5 text-cyan-400" />
|
||||||
|
<span className="text-base font-bold text-white tracking-tight">OMNIOIL</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{pendingCount > 0 && (
|
||||||
|
<span className="flex items-center gap-1 rounded-full bg-amber-500/10 border border-amber-500/20 px-2 py-0.5 text-xs font-medium text-amber-400">
|
||||||
|
<RefreshCw className="h-3 w-3" />
|
||||||
|
{pendingCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={`h-2 w-2 rounded-full ${isOnline ? 'bg-emerald-400' : 'bg-red-400'}`}
|
||||||
|
title={isOnline ? 'Online' : 'Sin conexión'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<main className="flex-1 overflow-y-auto p-4">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Bottom Nav */}
|
||||||
|
<nav className="flex border-t border-[var(--border)] bg-[var(--card)] shrink-0">
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<NavLink
|
||||||
|
key={item.to}
|
||||||
|
to={item.to}
|
||||||
|
end={item.end}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`flex flex-1 flex-col items-center justify-center gap-1 py-2.5 text-[10px] font-medium transition-colors ${
|
||||||
|
isActive ? 'text-cyan-400' : 'text-gray-500 hover:text-gray-300'
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<item.icon className="h-5 w-5" />
|
||||||
|
{item.label}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { createBrowserRouter, Navigate } from 'react-router-dom'
|
import { createBrowserRouter, Navigate } from 'react-router-dom'
|
||||||
import LoginPage from '@/features/auth/pages/LoginPage'
|
import LoginPage from '@/features/auth/pages/LoginPage'
|
||||||
import AdminLayout from '@/app/layouts/AdminLayout'
|
import AdminLayout from '@/app/layouts/AdminLayout'
|
||||||
|
import FieldLayout from '@/app/layouts/FieldLayout'
|
||||||
|
import OfflinePage from '@/features/offline/OfflinePage'
|
||||||
import DashboardPage from '@/features/admin/dashboard/pages/DashboardPage'
|
import DashboardPage from '@/features/admin/dashboard/pages/DashboardPage'
|
||||||
import ProjectListPage from '@/features/admin/projects/pages/ProjectListPage'
|
import ProjectListPage from '@/features/admin/projects/pages/ProjectListPage'
|
||||||
import CreateProjectPage from '@/features/admin/projects/pages/CreateProjectPage'
|
import CreateProjectPage from '@/features/admin/projects/pages/CreateProjectPage'
|
||||||
@@ -24,6 +26,10 @@ export const router = createBrowserRouter([
|
|||||||
path: '/login',
|
path: '/login',
|
||||||
element: <LoginPage />,
|
element: <LoginPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/offline',
|
||||||
|
element: <OfflinePage />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/app/admin',
|
path: '/app/admin',
|
||||||
element: <ProtectedRoute allowedRoles={['admin']} />,
|
element: <ProtectedRoute allowedRoles={['admin']} />,
|
||||||
@@ -48,7 +54,16 @@ export const router = createBrowserRouter([
|
|||||||
path: '/app/field',
|
path: '/app/field',
|
||||||
element: <ProtectedRoute allowedRoles={['operador']} />,
|
element: <ProtectedRoute allowedRoles={['operador']} />,
|
||||||
children: [
|
children: [
|
||||||
{ index: true, element: <ComingSoon title="Portal de Campo" /> },
|
{
|
||||||
|
element: <FieldLayout />,
|
||||||
|
children: [
|
||||||
|
{ index: true, element: <ComingSoon title="Portal de Campo" /> },
|
||||||
|
{ path: 'measurements', element: <ComingSoon title="Registro de Medición" /> },
|
||||||
|
{ path: 'movements', element: <ComingSoon title="Registro de Movimientos" /> },
|
||||||
|
{ path: 'well-shutdowns', element: <ComingSoon title="Paradas de Pozo" /> },
|
||||||
|
{ path: 'sync', element: <ComingSoon title="Estado de Sincronización" /> },
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
41
services/frontend/src/features/offline/OfflinePage.tsx
Normal file
41
services/frontend/src/features/offline/OfflinePage.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { WifiOff, RefreshCw } from 'lucide-react'
|
||||||
|
import { useSyncStore } from '@/lib/sync/sync-store'
|
||||||
|
import { useNetworkStatus } from '@/lib/sync/use-network-status'
|
||||||
|
|
||||||
|
export default function OfflinePage() {
|
||||||
|
useNetworkStatus()
|
||||||
|
|
||||||
|
const isOnline = useSyncStore((s) => s.isOnline)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const wasOffline = useRef(!isOnline)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOnline && wasOffline.current) {
|
||||||
|
navigate('/app/field', { replace: true })
|
||||||
|
}
|
||||||
|
if (!isOnline) {
|
||||||
|
wasOffline.current = true
|
||||||
|
}
|
||||||
|
}, [isOnline, navigate])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-screen bg-[var(--background)] px-6 text-center">
|
||||||
|
<div className="rounded-full bg-red-500/10 p-6 mb-6">
|
||||||
|
<WifiOff className="h-12 w-12 text-red-400" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-2xl font-bold text-white mb-2">Sin conexión</h1>
|
||||||
|
<p className="text-gray-400 mb-8 max-w-xs">
|
||||||
|
No hay conexión a internet. Tus datos guardados localmente están seguros.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="flex items-center gap-2 rounded-lg bg-cyan-600 px-5 py-2.5 text-sm font-semibold text-white hover:bg-cyan-500 transition-all"
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
Reintentar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
42
services/frontend/src/lib/db/index.ts
Normal file
42
services/frontend/src/lib/db/index.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import Dexie, { type Table } from 'dexie'
|
||||||
|
|
||||||
|
export interface SyncQueueItem {
|
||||||
|
id: string
|
||||||
|
operation_type: 'measurement' | 'movement' | 'shutdown'
|
||||||
|
payload: object
|
||||||
|
timestamp: number
|
||||||
|
status: 'draft' | 'pending_sync' | 'syncing' | 'synced' | 'rejected' | 'conflicted'
|
||||||
|
retry_count: number
|
||||||
|
last_error: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormDraft {
|
||||||
|
id: string
|
||||||
|
form_type: 'measurement' | 'movement' | 'shutdown'
|
||||||
|
asset_id: string
|
||||||
|
data: object
|
||||||
|
saved_at: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReferenceDataEntry {
|
||||||
|
key: string
|
||||||
|
data: object
|
||||||
|
cached_at: number
|
||||||
|
}
|
||||||
|
|
||||||
|
class OmniOilDB extends Dexie {
|
||||||
|
sync_queue!: Table<SyncQueueItem, string>
|
||||||
|
form_drafts!: Table<FormDraft, string>
|
||||||
|
reference_data!: Table<ReferenceDataEntry, string>
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('omni-oil-db')
|
||||||
|
this.version(1).stores({
|
||||||
|
sync_queue: 'id, status, timestamp',
|
||||||
|
form_drafts: 'id, form_type, asset_id',
|
||||||
|
reference_data: 'key',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const db = new OmniOilDB()
|
||||||
59
services/frontend/src/lib/sync/sync-store.ts
Normal file
59
services/frontend/src/lib/sync/sync-store.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { create } from 'zustand'
|
||||||
|
import { db, type SyncQueueItem } from '@/lib/db'
|
||||||
|
|
||||||
|
interface SyncState {
|
||||||
|
isOnline: boolean
|
||||||
|
pendingCount: number
|
||||||
|
lastSync: number | null
|
||||||
|
setOnline: (online: boolean) => void
|
||||||
|
setPendingCount: (count: number) => void
|
||||||
|
setLastSync: (ts: number) => void
|
||||||
|
refreshPendingCount: () => Promise<void>
|
||||||
|
addToQueue: (entry: Pick<SyncQueueItem, 'operation_type' | 'payload' | 'status'>) => Promise<void>
|
||||||
|
updateQueueItem: (id: string, patch: Partial<SyncQueueItem>) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSyncStore = create<SyncState>()((set) => ({
|
||||||
|
isOnline: navigator.onLine,
|
||||||
|
pendingCount: 0,
|
||||||
|
lastSync: null,
|
||||||
|
|
||||||
|
setOnline: (online) => set({ isOnline: online }),
|
||||||
|
setPendingCount: (count) => set({ pendingCount: count }),
|
||||||
|
setLastSync: (ts) => set({ lastSync: ts }),
|
||||||
|
|
||||||
|
refreshPendingCount: async () => {
|
||||||
|
const count = await db.sync_queue
|
||||||
|
.where('status')
|
||||||
|
.anyOf(['pending_sync', 'syncing'])
|
||||||
|
.count()
|
||||||
|
set({ pendingCount: count })
|
||||||
|
},
|
||||||
|
|
||||||
|
addToQueue: async (entry) => {
|
||||||
|
const item: SyncQueueItem = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
operation_type: entry.operation_type,
|
||||||
|
payload: entry.payload,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
status: entry.status ?? 'pending_sync',
|
||||||
|
retry_count: 0,
|
||||||
|
last_error: null,
|
||||||
|
}
|
||||||
|
await db.sync_queue.add(item)
|
||||||
|
const count = await db.sync_queue
|
||||||
|
.where('status')
|
||||||
|
.anyOf(['pending_sync', 'syncing'])
|
||||||
|
.count()
|
||||||
|
set({ pendingCount: count })
|
||||||
|
},
|
||||||
|
|
||||||
|
updateQueueItem: async (id, patch) => {
|
||||||
|
await db.sync_queue.update(id, patch)
|
||||||
|
const count = await db.sync_queue
|
||||||
|
.where('status')
|
||||||
|
.anyOf(['pending_sync', 'syncing'])
|
||||||
|
.count()
|
||||||
|
set({ pendingCount: count })
|
||||||
|
},
|
||||||
|
}))
|
||||||
23
services/frontend/src/lib/sync/use-network-status.ts
Normal file
23
services/frontend/src/lib/sync/use-network-status.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useSyncStore } from './sync-store'
|
||||||
|
|
||||||
|
export function useNetworkStatus(): void {
|
||||||
|
const setOnline = useSyncStore((s) => s.setOnline)
|
||||||
|
const refreshPendingCount = useSyncStore((s) => s.refreshPendingCount)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOnline(navigator.onLine)
|
||||||
|
refreshPendingCount()
|
||||||
|
|
||||||
|
const handleOnline = () => setOnline(true)
|
||||||
|
const handleOffline = () => setOnline(false)
|
||||||
|
|
||||||
|
window.addEventListener('online', handleOnline)
|
||||||
|
window.addEventListener('offline', handleOffline)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('online', handleOnline)
|
||||||
|
window.removeEventListener('offline', handleOffline)
|
||||||
|
}
|
||||||
|
}, [setOnline, refreshPendingCount])
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { registerSW } from 'virtual:pwa-register'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import { RouterProvider } from 'react-router-dom'
|
import { RouterProvider } from 'react-router-dom'
|
||||||
@@ -14,6 +15,8 @@ const queryClient = new QueryClient({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
registerSW({ immediate: true })
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
|||||||
1
services/frontend/src/vite-env.d.ts
vendored
1
services/frontend/src/vite-env.d.ts
vendored
@@ -1 +1,2 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference types="vite-plugin-pwa/client" />
|
||||||
|
|||||||
@@ -1,9 +1,48 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [
|
||||||
|
react(),
|
||||||
|
VitePWA({
|
||||||
|
registerType: 'autoUpdate',
|
||||||
|
devOptions: { enabled: true },
|
||||||
|
manifest: {
|
||||||
|
name: 'OmniOil',
|
||||||
|
short_name: 'OmniOil',
|
||||||
|
theme_color: '#06b6d4',
|
||||||
|
background_color: '#0f172a',
|
||||||
|
id: '/',
|
||||||
|
display: 'standalone',
|
||||||
|
start_url: '/',
|
||||||
|
icons: [
|
||||||
|
{ src: '/pwa-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
|
||||||
|
{ src: '/pwa-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
|
||||||
|
{ src: '/pwa-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
workbox: {
|
||||||
|
globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
|
||||||
|
runtimeCaching: [
|
||||||
|
{
|
||||||
|
urlPattern: /^https?:\/\/.*\/api\/assets/,
|
||||||
|
handler: 'StaleWhileRevalidate',
|
||||||
|
options: {
|
||||||
|
cacheName: 'omnioil-reference',
|
||||||
|
expiration: { maxAgeSeconds: 86400 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urlPattern: /^https?:\/\/.*\/api\/auth/,
|
||||||
|
handler: 'NetworkFirst',
|
||||||
|
options: { cacheName: 'omnioil-auth' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, './src'),
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
|||||||
Reference in New Issue
Block a user