Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:50:06 +08:00
commit 5e3ca965d9
68 changed files with 11257 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
{
"name": "odoo-pwa-generator",
"description": "Generate offline-first Progressive Web Apps with Odoo Studio backend integration. Supports SvelteKit, React, and Vue with smart caching, IndexedDB storage, and automatic sync.",
"version": "1.0.0",
"author": {
"name": "Jamshid",
"url": "https://github.com/jamshu"
},
"skills": [
"./skills"
],
"commands": [
"./commands"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# odoo-pwa-generator
Generate offline-first Progressive Web Apps with Odoo Studio backend integration. Supports SvelteKit, React, and Vue with smart caching, IndexedDB storage, and automatic sync.

534
commands/add-deployment.md Normal file
View File

@@ -0,0 +1,534 @@
Add a new deployment target to your existing Odoo PWA project.
## What this command does:
- Adds deployment configuration for new platforms
- Creates necessary config files
- Sets up CI/CD workflows
- Provides deployment instructions
- Configures environment variables
## Supported Deployment Targets
### Primary (Recommended)
-**Vercel** - Best for full-stack PWAs (API routes work)
-**Netlify** - Great for static + serverless functions
-**Cloudflare Pages** - Fast global CDN, edge functions
### Secondary (Static Only)
- ⚠️ **GitHub Pages** - Free, but no server-side code
- ⚠️ **Cloudflare Pages (Static)** - Without edge functions
- ⚠️ **AWS S3 + CloudFront** - Static hosting only
---
## Prerequisites
Before adding deployment:
```
□ Project builds successfully (npm run build)
□ Odoo connection tested and working
□ Environment variables documented
□ Git repository initialized
□ Code committed to version control
```
---
## Add Vercel Deployment 🔷
### What You'll Get:
- Automatic deployments from Git
- Serverless API routes
- Preview deployments for PRs
- Environment variable management
- Custom domains
- Analytics and logs
### Steps:
#### 1. Install Vercel CLI (Optional)
```bash
npm install -g vercel
```
#### 2. Create `vercel.json`
```json
{
"buildCommand": "npm run build",
"outputDirectory": "build",
"framework": "sveltekit",
"installCommand": "npm install",
"devCommand": "npm run dev",
"env": {
"VITE_ODOO_URL": "@vite_odoo_url",
"VITE_ODOO_DB": "@vite_odoo_db",
"VITE_MODEL_NAME": "@vite_model_name",
"VITE_MODEL_DISPLAY_NAME": "@vite_model_display_name"
},
"build": {
"env": {
"ODOO_API_KEY": "@odoo_api_key",
"ODOO_USERNAME": "@odoo_username"
}
}
}
```
For React/Vue:
```json
{
"buildCommand": "npm run build",
"outputDirectory": "dist",
"framework": "vite"
}
```
#### 3. Create `.vercelignore`
```
node_modules
.env
.env.local
*.log
.DS_Store
```
#### 4. Deployment Options
**Option A: Vercel Dashboard (Recommended for first time)**
1. Go to https://vercel.com/new
2. Connect your Git repository
3. Vercel auto-detects framework
4. Add environment variables:
- `VITE_ODOO_URL`
- `VITE_ODOO_DB`
- `ODOO_API_KEY`
- `ODOO_USERNAME`
- `VITE_MODEL_NAME`
- `VITE_MODEL_DISPLAY_NAME`
5. Click "Deploy"
**Option B: Vercel CLI**
```bash
vercel login
vercel
# Follow prompts
# Add environment variables when asked
# Or add them later in dashboard
```
**Option C: Continuous Deployment**
1. Connect repository to Vercel
2. Every push to `main` auto-deploys
3. PRs get preview deployments
#### 5. Configure Environment Variables
In Vercel Dashboard:
1. Go to Project Settings
2. Environment Variables tab
3. Add each variable:
- Production
- Preview (optional)
- Development (optional)
#### 6. Test Deployment
1. Wait for build to complete
2. Visit deployed URL
3. Test Odoo connection
4. Verify all features work
5. Check browser console for errors
---
## Add Netlify Deployment 🟢
### What You'll Get:
- Git-based deployments
- Serverless functions
- Form handling
- Split testing
- Deploy previews
- Custom domains
### Steps:
#### 1. Create `netlify.toml`
```toml
[build]
command = "npm run build"
publish = "build"
[build.environment]
NODE_VERSION = "18"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
[functions]
directory = "netlify/functions"
```
For React/Vue:
```toml
[build]
command = "npm run build"
publish = "dist"
```
#### 2. Convert API Routes to Netlify Functions
Create `netlify/functions/odoo.js`:
```javascript
// Copy your API route logic here
// Netlify functions use different format
exports.handler = async (event, context) => {
// Parse request
const body = JSON.parse(event.body);
// Your Odoo logic here
// (copy from src/routes/api/odoo/+server.js)
return {
statusCode: 200,
body: JSON.stringify(result)
};
};
```
#### 3. Update Client to Use Netlify Function
```javascript
// Change API endpoint
const response = await fetch('/.netlify/functions/odoo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
```
#### 4. Deploy
**Option A: Netlify Dashboard**
1. Go to https://app.netlify.com/start
2. Connect repository
3. Configure build settings
4. Add environment variables
5. Deploy
**Option B: Netlify CLI**
```bash
npm install -g netlify-cli
netlify login
netlify init
netlify deploy --prod
```
---
## Add Cloudflare Pages Deployment 🟠
### What You'll Get:
- Global CDN
- Unlimited bandwidth
- Edge functions
- Preview deployments
- Web Analytics
- Fast performance
### Steps:
#### 1. Create `wrangler.toml` (for edge functions)
```toml
name = "odoo-pwa"
compatibility_date = "2025-01-01"
[build]
command = "npm run build"
[build.upload]
format = "service-worker"
```
#### 2. Convert API Routes to Workers
Create `functions/odoo.js`:
```javascript
export async function onRequest(context) {
const { request, env } = context;
// Parse request
const body = await request.json();
// Odoo logic here
// Access env vars via env.ODOO_API_KEY
return new Response(JSON.stringify(result), {
headers: { 'Content-Type': 'application/json' }
});
}
```
#### 3. Deploy
**Option A: Cloudflare Dashboard**
1. Go to Cloudflare Pages
2. Connect Git repository
3. Configure build:
- Build command: `npm run build`
- Output: `build` or `dist`
4. Add environment variables
5. Deploy
**Option B: Wrangler CLI**
```bash
npm install -g wrangler
wrangler login
wrangler pages project create odoo-pwa
wrangler pages publish build
```
---
## Add GitHub Pages Deployment 📘
### ⚠️ Limitations:
- Static hosting only
- No server-side API routes
- Must modify Odoo client for direct API calls
- CORS issues possible
### When to Use:
- Demo projects
- Public apps (no sensitive data)
- Frontend-only versions
### Steps:
#### 1. Update Base Path
**SvelteKit** (`svelte.config.js`):
```javascript
const config = {
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: 'index.html'
}),
paths: {
base: process.env.NODE_ENV === 'production'
? '/your-repo-name'
: ''
}
}
};
```
**React/Vue** (`vite.config.js`):
```javascript
export default {
base: '/your-repo-name/'
};
```
#### 2. Create `.github/workflows/deploy.yml`
```yaml
name: Deploy to GitHub Pages
on:
push:
branches: [main]
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install
- run: npm run build
env:
VITE_ODOO_URL: ${{ secrets.VITE_ODOO_URL }}
VITE_ODOO_DB: ${{ secrets.VITE_ODOO_DB }}
VITE_MODEL_NAME: ${{ secrets.VITE_MODEL_NAME }}
- uses: actions/upload-pages-artifact@v3
with:
path: build
deploy:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/deploy-pages@v4
```
#### 3. Configure Repository
1. Go to Settings → Pages
2. Source: GitHub Actions
3. Save
#### 4. Add Secrets
1. Settings → Secrets and variables → Actions
2. Add each environment variable
#### 5. Push to Deploy
```bash
git add .
git commit -m "Add GitHub Pages deployment"
git push origin main
```
---
## Environment Variables Reference
### Required for All Platforms:
```bash
# Public (VITE_ prefix for client access)
VITE_ODOO_URL=https://yourcompany.odoo.com
VITE_ODOO_DB=yourcompany-main
VITE_MODEL_NAME=x_expense
VITE_MODEL_DISPLAY_NAME=Expense
# Private (server-side only)
ODOO_API_KEY=your_production_api_key
ODOO_USERNAME=your.email@company.com
```
### Platform-Specific:
**Vercel:**
- Add in Project Settings → Environment Variables
- Separate for Production/Preview/Development
**Netlify:**
- Add in Site Settings → Environment Variables
- Or in `netlify.toml`
**Cloudflare:**
- Add in Pages → Settings → Environment Variables
- Or use Wrangler secrets
**GitHub Pages:**
- Add in Repository Settings → Secrets
- Used in GitHub Actions workflow
---
## Post-Deployment Checklist ✅
After deploying to new platform:
```
□ Build completed successfully
□ Application loads at deployment URL
□ Odoo connection works
□ Data syncs correctly
□ CRUD operations work
□ Offline mode functions
□ Service worker registered
□ PWA installs correctly
□ Environment variables set correctly
□ No console errors
□ Tested on mobile
□ Custom domain configured (if needed)
```
---
## Multi-Platform Deployment
### Deploy to Multiple Platforms:
You can deploy the same app to multiple platforms for:
- Redundancy
- A/B testing
- Different regions
- Different audiences
### Example Setup:
```
main branch → Vercel (primary production)
staging branch → Netlify (staging)
PRs → Vercel preview deployments
```
---
## Troubleshooting Deployment
### Build Fails
- Check build logs
- Test `npm run build` locally
- Verify Node version matches
- Check for missing env vars
### App Loads but Doesn't Work
- Check environment variables are set
- Look at browser console errors
- Verify API routes deployed correctly
- Test Odoo connection
### API Routes Not Working
- Verify platform supports server-side code
- Check function logs
- Ensure correct paths used
- Test API endpoint directly
---
## Example prompts to use this command:
- `/add-deployment` - Add new deployment target
- User: "Deploy to Netlify"
- User: "Add Cloudflare deployment"
- User: "Set up continuous deployment"
## Related Commands:
- `/deploy-vercel` - Deploy to Vercel
- `/deploy-github` - Deploy to GitHub Pages
- `/test-connection` - Test before deploying
- `/optimize` - Optimize before production
---
## Best Practices
1. **Use Git-Based Deployment**
- Automatic on push
- Preview deployments
- Easy rollbacks
2. **Separate Environments**
- Development
- Staging
- Production
3. **Secure Secrets**
- Never commit API keys
- Use platform secret management
- Rotate keys regularly
4. **Monitor Deployments**
- Set up error tracking
- Monitor performance
- Watch build times
5. **Test Before Merging**
- Use preview deployments
- Test all features
- Check on real devices

46
commands/add-model.md Normal file
View File

@@ -0,0 +1,46 @@
Add integration for a new Odoo Studio model to an existing PWA project.
## What this command does:
- Invokes the `add-odoo-model` skill
- Creates cache store for the new model
- Adds API methods to the Odoo client
- Generates form and list pages (optional)
- Updates navigation and routing
- Maintains consistency with existing project structure
## Required Information:
Before starting, gather:
1. **Current working directory** - Must be inside an existing Odoo PWA project
2. **Framework** - Detect from project files (SvelteKit/React/Vue)
3. **New model name** (without `x_` prefix, e.g., "task", "product")
4. **Model display name** (human-readable, e.g., "Task", "Product")
5. **Generate UI?** - Whether to create form and list pages (yes/no)
## Steps:
1. Verify the current directory is an Odoo PWA project (check for odoo.js, cache stores)
2. Detect the framework from project structure
3. Ask the user for the new model details
4. Create cache store for the model in `src/lib/stores/` or equivalent
5. Update the Odoo API client with methods for the new model
6. If requested, generate form and list pages/components
7. Update navigation/routing if UI was generated
8. Update documentation with the new model
## Example prompts to use this command:
- `/add-model` - Interactive mode, will ask for all parameters
- User: "Add a task model to my PWA"
- User: "Integrate product catalog from Odoo"
## Validation:
Before proceeding, check:
- Project has `src/lib/odoo.js` or equivalent
- Project has existing cache stores
- `.env` file exists with Odoo configuration
- Framework can be detected
## After adding model:
Remind the user to:
1. Update `.env` with any new model-specific configuration
2. Test the new model's CRUD operations
3. Verify sync functionality works correctly
4. Update any necessary business logic or calculations

734
commands/api-reference.md Normal file
View File

@@ -0,0 +1,734 @@
Complete API reference for the Odoo client and cache stores in generated PWAs.
## What this command does:
- Provides comprehensive API documentation
- Lists all available methods and functions
- Shows parameter types and return values
- Includes code examples for each method
- Helps developers use the generated code effectively
---
## Odoo API Client Reference 🔌
The Odoo API client (`src/lib/odoo.js` or equivalent) provides methods for interacting with Odoo Studio models.
### Configuration
#### Environment Variables
```bash
VITE_ODOO_URL=https://yourcompany.odoo.com
VITE_ODOO_DB=yourcompany-main
ODOO_API_KEY=your_api_key
ODOO_USERNAME=your.email@company.com
VITE_MODEL_NAME=x_expense
```
---
## CRUD Operations
### createRecord()
Create a new record in Odoo.
**Signature:**
```javascript
async function createRecord(model, fields)
```
**Parameters:**
- `model` (string) - Odoo model name (e.g., 'x_expense')
- `fields` (object) - Field values to set
**Returns:** Promise<number> - ID of created record
**Example:**
```javascript
const newId = await odoo.createRecord('x_expense', {
x_studio_description: 'Team lunch',
x_studio_amount: 45.50,
x_studio_date: '2025-01-15',
x_studio_category: 'meal',
x_studio_employee: odoo.formatMany2one(12)
});
console.log(`Created expense with ID: ${newId}`);
```
**Error Handling:**
```javascript
try {
const id = await odoo.createRecord('x_expense', fields);
} catch (error) {
console.error('Failed to create:', error.message);
// Handle error (show message to user, retry, etc.)
}
```
---
### searchRecords()
Search and read records from Odoo.
**Signature:**
```javascript
async function searchRecords(model, domain, fields, limit = null, offset = null)
```
**Parameters:**
- `model` (string) - Odoo model name
- `domain` (array) - Odoo domain filter (e.g., [['id', '>', 100]])
- `fields` (array) - List of field names to fetch
- `limit` (number, optional) - Maximum number of records
- `offset` (number, optional) - Number of records to skip
**Returns:** Promise<Array<Object>> - Array of record objects
**Example:**
```javascript
// Get all expenses
const allExpenses = await odoo.searchRecords(
'x_expense',
[],
['x_studio_description', 'x_studio_amount', 'x_studio_date']
);
// Get expenses > $100
const largeExpenses = await odoo.searchRecords(
'x_expense',
[['x_studio_amount', '>', 100]],
['x_studio_description', 'x_studio_amount']
);
// Get recent 10 expenses
const recentExpenses = await odoo.searchRecords(
'x_expense',
[],
['x_studio_description', 'x_studio_date'],
10 // limit
);
// Get expenses with pagination
const page2 = await odoo.searchRecords(
'x_expense',
[],
fields,
20, // limit: 20 per page
20 // offset: skip first 20 (page 2)
);
```
**Domain Syntax:**
```javascript
// Equals
[['x_studio_status', '=', 'draft']]
// Greater than
[['x_studio_amount', '>', 100]]
// In list
[['x_studio_category', 'in', ['meal', 'travel']]]
// Multiple conditions (AND)
[
['x_studio_amount', '>', 50],
['x_studio_status', '=', 'draft']
]
// OR conditions
['|',
['x_studio_amount', '>', 100],
['x_studio_category', '=', 'travel']
]
// Complex: (amount > 100 OR category = travel) AND status = draft
['&',
'|',
['x_studio_amount', '>', 100],
['x_studio_category', '=', 'travel'],
['x_studio_status', '=', 'draft']
]
```
---
### updateRecord()
Update an existing record.
**Signature:**
```javascript
async function updateRecord(model, id, values)
```
**Parameters:**
- `model` (string) - Odoo model name
- `id` (number) - Record ID to update
- `values` (object) - Fields to update
**Returns:** Promise<boolean> - true if successful
**Example:**
```javascript
await odoo.updateRecord('x_expense', 123, {
x_studio_status: 'approved',
x_studio_amount: 55.00
});
// Update multiple fields
await odoo.updateRecord('x_expense', 123, {
x_studio_description: 'Updated description',
x_studio_date: '2025-01-20',
x_studio_notes: 'Added receipt'
});
```
---
### deleteRecord()
Delete a record from Odoo.
**Signature:**
```javascript
async function deleteRecord(model, id)
```
**Parameters:**
- `model` (string) - Odoo model name
- `id` (number) - Record ID to delete
**Returns:** Promise<boolean> - true if successful
**Example:**
```javascript
await odoo.deleteRecord('x_expense', 123);
// With confirmation
if (confirm('Are you sure you want to delete this expense?')) {
await odoo.deleteRecord('x_expense', expenseId);
}
```
**Error Handling:**
```javascript
try {
await odoo.deleteRecord('x_expense', id);
console.log('Deleted successfully');
} catch (error) {
console.error('Failed to delete:', error.message);
alert('Could not delete: ' + error.message);
}
```
---
## Helper Methods
### fetchPartners()
Fetch partner (res.partner) records.
**Signature:**
```javascript
async function fetchPartners(ids = null)
```
**Parameters:**
- `ids` (array, optional) - Specific partner IDs to fetch. If null, fetches all.
**Returns:** Promise<Array<Object>> - Array of partner objects
**Example:**
```javascript
// Fetch all partners
const allPartners = await odoo.fetchPartners();
// Fetch specific partners
const somePartners = await odoo.fetchPartners([1, 2, 3]);
// Use in dropdown
const partners = await odoo.fetchPartners();
// Display: partners.map(p => ({ value: p.id, label: p.name }))
```
---
### formatMany2one()
Format a Many2one field value for Odoo.
**Signature:**
```javascript
function formatMany2one(id)
```
**Parameters:**
- `id` (number | null) - Partner/record ID
**Returns:** Array<number, boolean> | false - Odoo-formatted value
**Example:**
```javascript
// Set employee field
const fields = {
x_studio_employee: odoo.formatMany2one(12)
// Result: [12, false]
};
// Clear employee field
const fields = {
x_studio_employee: odoo.formatMany2one(null)
// Result: false
};
```
---
### formatMany2many()
Format a Many2many field value for Odoo.
**Signature:**
```javascript
function formatMany2many(ids)
```
**Parameters:**
- `ids` (array) - Array of record IDs
**Returns:** Array - Odoo command format
**Example:**
```javascript
// Set tags (replace all)
const fields = {
x_studio_tags: odoo.formatMany2many([1, 2, 3])
// Result: [[6, 0, [1, 2, 3]]]
};
// Clear tags
const fields = {
x_studio_tags: odoo.formatMany2many([])
// Result: [[6, 0, []]]
};
```
**Odoo Many2many Commands:**
```javascript
// (6, 0, [ids]) - Replace all (what formatMany2many uses)
// (4, id) - Add link to id
// (3, id) - Remove link to id
// (5, 0) - Remove all links
```
---
## Cache Store API Reference 💾
The cache store provides reactive state management with offline-first capabilities.
### Properties
#### records
**Type:** Reactive Array<Object>
Current cached records.
**Example:**
```javascript
// SvelteKit
$: totalAmount = $expenseCache.reduce((sum, e) => sum + e.x_studio_amount, 0);
// React
const totalAmount = useMemo(() =>
records.reduce((sum, e) => sum + e.x_studio_amount, 0),
[records]
);
// Vue
const totalAmount = computed(() =>
expenseStore.records.reduce((sum, e) => sum + e.x_studio_amount, 0)
);
```
#### isLoading
**Type:** Reactive Boolean
Loading state indicator.
**Example:**
```javascript
{#if $expenseCache.isLoading}
<LoadingSpinner />
{:else}
<ExpenseList />
{/if}
```
#### error
**Type:** Reactive String | null
Current error message, if any.
**Example:**
```javascript
{#if $expenseCache.error}
<ErrorAlert message={$expenseCache.error} />
{/if}
```
#### lastSync
**Type:** Reactive Number (timestamp)
Timestamp of last successful sync.
**Example:**
```javascript
const timeSinceSync = Date.now() - $expenseCache.lastSync;
const minutes = Math.floor(timeSinceSync / 60000);
// Display: "Last synced ${minutes} minutes ago"
```
---
### Methods
### load()
Load records from cache and trigger background sync.
**Signature:**
```javascript
async function load()
```
**Returns:** Promise<void>
**Behavior:**
1. Loads from cache immediately (instant UI update)
2. Checks if cache is stale (> 5 minutes)
3. If stale, syncs in background
4. Updates UI when new data arrives
**Example:**
```javascript
// SvelteKit
$effect(() => {
expenseCache.load();
});
// React
useEffect(() => {
expenseCache.load();
}, []);
// Vue
onMounted(() => {
expenseStore.load();
});
```
---
### create()
Create a new record with optimistic update.
**Signature:**
```javascript
async function create(data)
```
**Parameters:**
- `data` (object) - Field values for new record
**Returns:** Promise<number> - ID of created record
**Behavior:**
1. Generates temporary ID
2. Adds to cache immediately (optimistic)
3. Creates in Odoo (background)
4. Replaces temp ID with real ID
5. Rolls back on error
**Example:**
```javascript
try {
const newId = await expenseCache.create({
x_studio_description: 'Lunch meeting',
x_studio_amount: 45.50,
x_studio_date: '2025-01-15',
x_studio_category: 'meal'
});
console.log('Created:', newId);
navigate(`/expenses/${newId}`);
} catch (error) {
alert('Failed to create: ' + error.message);
}
```
---
### update()
Update an existing record.
**Signature:**
```javascript
async function update(id, data)
```
**Parameters:**
- `id` (number) - Record ID
- `data` (object) - Fields to update
**Returns:** Promise<boolean>
**Behavior:**
1. Updates cache immediately (optimistic)
2. Updates in Odoo (background)
3. Rolls back on error
**Example:**
```javascript
await expenseCache.update(123, {
x_studio_amount: 50.00,
x_studio_status: 'submitted'
});
```
---
### delete()
Delete a record.
**Signature:**
```javascript
async function remove(id)
```
**Parameters:**
- `id` (number) - Record ID to delete
**Returns:** Promise<boolean>
**Behavior:**
1. Removes from cache immediately
2. Deletes from Odoo (background)
3. Restores on error
**Example:**
```javascript
if (confirm('Delete this expense?')) {
try {
await expenseCache.remove(123);
navigate('/expenses');
} catch (error) {
alert('Failed to delete: ' + error.message);
}
}
```
---
### refresh()
Force refresh from Odoo.
**Signature:**
```javascript
async function refresh()
```
**Returns:** Promise<void>
**Behavior:**
1. Fetches all records from Odoo
2. Replaces cache
3. Updates UI
**Example:**
```javascript
// Manual refresh button
<button onclick={() => expenseCache.refresh()}>
Refresh
</button>
```
---
### clearCache()
Clear all cached data.
**Signature:**
```javascript
function clearCache()
```
**Returns:** void
**Behavior:**
1. Clears localStorage
2. Clears IndexedDB
3. Resets records to empty array
**Example:**
```javascript
// Logout function
async function logout() {
expenseCache.clearCache();
// Clear other caches
navigate('/login');
}
```
---
## Advanced Patterns
### Custom Filters
```javascript
// Derived store (SvelteKit)
import { derived } from 'svelte/store';
export const draftExpenses = derived(
expenseCache,
$cache => $cache.filter(e => e.x_studio_status === 'draft')
);
// Hook (React)
function useDraftExpenses() {
const { records } = useExpense();
return useMemo(
() => records.filter(e => e.x_studio_status === 'draft'),
[records]
);
}
// Computed (Vue)
const draftExpenses = computed(() =>
expenseStore.records.filter(e => e.x_studio_status === 'draft')
);
```
### Sorting
```javascript
export const sortedExpenses = derived(
expenseCache,
$cache => [...$cache].sort((a, b) =>
b.x_studio_date.localeCompare(a.x_studio_date)
)
);
```
### Search
```javascript
function searchExpenses(query) {
return records.filter(e =>
e.x_studio_description.toLowerCase().includes(query.toLowerCase())
);
}
```
### Grouping
```javascript
function groupByCategory(records) {
return records.reduce((groups, record) => {
const category = record.x_studio_category;
if (!groups[category]) groups[category] = [];
groups[category].push(record);
return groups;
}, {});
}
```
### Aggregation
```javascript
function getTotalByCategory(records) {
return records.reduce((totals, record) => {
const cat = record.x_studio_category;
totals[cat] = (totals[cat] || 0) + record.x_studio_amount;
return totals;
}, {});
}
```
---
## Server Route API Reference 🔐
The server route (`src/routes/api/odoo/+server.js`) handles Odoo communication.
### Endpoint
**URL:** `/api/odoo`
**Method:** POST
**Content-Type:** application/json
### Request Format
```json
{
"action": "create|search|update|delete",
"model": "x_expense",
...parameters
}
```
### Actions
#### create
```json
{
"action": "create",
"model": "x_expense",
"fields": {
"x_studio_description": "Lunch",
"x_studio_amount": 45.50
}
}
```
**Response:** `{ "id": 123 }`
#### search
```json
{
"action": "search",
"model": "x_expense",
"domain": [["id", ">", 100]],
"fields": ["x_studio_description", "x_studio_amount"]
}
```
**Response:** `{ "records": [...] }`
#### update
```json
{
"action": "update",
"model": "x_expense",
"id": 123,
"values": {
"x_studio_amount": 50.00
}
}
```
**Response:** `{ "success": true }`
#### delete
```json
{
"action": "delete",
"model": "x_expense",
"id": 123
}
```
**Response:** `{ "success": true }`
---
## Example prompts to use this command:
- `/api-reference` - Show complete API documentation
- User: "What methods are available in the Odoo client?"
- User: "How do I use the cache store?"
- User: "Show me API examples"
## Next Steps:
- Try the methods in your project
- Review `/examples` for practical use cases
- See `/architecture` for design patterns
- Check `/help` for more information

634
commands/architecture.md Normal file
View File

@@ -0,0 +1,634 @@
Detailed explanation of the Odoo PWA architecture, patterns, and design decisions.
## What this command does:
- Explains the architectural patterns used in generated PWAs
- Details the data flow and state management
- Describes the caching strategy
- Explains offline-first design principles
- Provides insights into technical decisions
---
## Architecture Overview 🏗️
The Odoo PWA Generator creates **offline-first Progressive Web Apps** with a **three-layer architecture** that ensures data availability, performance, and seamless Odoo integration.
```
┌─────────────────────────────────────────┐
│ UI Layer (Components) │
│ - Forms, Lists, Navigation │
│ - Framework-specific (Svelte/React/Vue)│
└──────────────┬──────────────────────────┘
┌─────────────────────────────────────────┐
│ State Layer (Cache Stores) │
│ - Smart Caching Logic │
│ - Dual Storage (localStorage + IndexedDB) │
│ - Background Sync │
│ - Optimistic Updates │
└──────────────┬──────────────────────────┘
┌─────────────────────────────────────────┐
│ API Layer (Odoo Client) │
│ - JSON-RPC Communication │
│ - CRUD Operations │
│ - Field Formatting │
└──────────────┬──────────────────────────┘
┌─────────────────────────────────────────┐
│ Server Layer (API Routes) │
│ - Credential Management │
│ - UID Caching │
│ - Error Handling │
└──────────────┬──────────────────────────┘
┌─────────────────────────────────────────┐
│ Odoo Backend │
│ - Studio Models │
│ - Business Logic │
│ - Data Persistence │
└─────────────────────────────────────────┘
```
---
## Layer 1: UI Components 🎨
### Purpose:
Present data to users and capture user input.
### Responsibilities:
- Render data from cache stores
- Handle user interactions
- Validate form inputs
- Display loading and error states
- Provide responsive, mobile-friendly interface
### Framework-Specific Implementation:
#### SvelteKit
```javascript
<script>
import { expenseCache } from '$lib/stores/expenseCache';
// Reactive to cache updates
$effect(() => {
expenseCache.load();
});
</script>
{#each $expenseCache as expense}
<ExpenseCard {expense} />
{/each}
```
#### React
```javascript
import { useExpense } from './contexts/ExpenseContext';
function ExpenseList() {
const { records, isLoading } = useExpense();
useEffect(() => {
// Load on mount
}, []);
return records.map(expense => (
<ExpenseCard key={expense.id} expense={expense} />
));
}
```
#### Vue
```javascript
<script setup>
import { useExpenseStore } from '@/stores/expenseStore';
const expenseStore = useExpenseStore();
expenseStore.load();
</script>
<template>
<ExpenseCard
v-for="expense in expenseStore.records"
:key="expense.id"
:expense="expense"
/>
</template>
```
### Design Principles:
- **Reactive by default** - UI updates automatically when data changes
- **Loading states** - Show skeleton loaders while data fetches
- **Error boundaries** - Graceful error handling
- **Optimistic UI** - Show changes immediately, sync in background
---
## Layer 2: Cache Stores (State Management) 💾
### Purpose:
Manage application state with offline-first caching.
### The Smart Caching Pattern:
```javascript
// Two-phase load strategy
export async function load() {
// Phase 1: Load from cache (instant)
const cached = await loadFromCache();
records.set(cached); // UI shows data immediately
// Phase 2: Check if stale and sync
if (isCacheStale()) {
await syncInBackground(); // Update in background
}
}
```
### Dual Storage Strategy:
#### localStorage (Metadata)
Stores lightweight metadata:
- `lastSyncTime` - When was last successful sync
- `lastRecordId` - Highest ID fetched so far
- `version` - Cache schema version
```javascript
localStorage.setItem('expenseCache', JSON.stringify({
lastSyncTime: Date.now(),
lastRecordId: 123,
version: 1
}));
```
#### IndexedDB (Master Data)
Stores actual records:
- All record data
- Larger storage capacity
- Async API
- Structured data
```javascript
await db.expenses.bulkPut(records);
```
### Stale Detection:
```javascript
function isCacheStale() {
const cacheData = JSON.parse(localStorage.getItem('expenseCache'));
const CACHE_VALIDITY = 5 * 60 * 1000; // 5 minutes
return !cacheData ||
(Date.now() - cacheData.lastSyncTime) > CACHE_VALIDITY;
}
```
### Incremental Sync Pattern:
```javascript
// Only fetch new records, not everything
async function syncInBackground() {
const { lastRecordId } = getCacheMetadata();
// Fetch only records with id > lastRecordId
const newRecords = await odoo.searchRecords(
'x_expense',
[['id', '>', lastRecordId]],
fields
);
if (newRecords.length > 0) {
await appendToCache(newRecords);
updateLastRecordId(newRecords[newRecords.length - 1].id);
}
}
```
### Optimistic Updates:
```javascript
export async function create(data) {
// 1. Generate temporary ID
const tempId = `temp-${Date.now()}`;
const tempRecord = { id: tempId, ...data };
// 2. Update UI immediately
records.update(r => [...r, tempRecord]);
// 3. Create in Odoo (background)
try {
const realId = await odoo.createRecord('x_expense', data);
// 4. Replace temp ID with real ID
records.update(r =>
r.map(rec => rec.id === tempId ? { ...rec, id: realId } : rec)
);
} catch (error) {
// 5. Rollback on error
records.update(r => r.filter(rec => rec.id !== tempId));
throw error;
}
}
```
### Background Sync Timer:
```javascript
let syncInterval;
export function startAutoSync() {
syncInterval = setInterval(() => {
syncInBackground();
}, 3 * 60 * 1000); // Every 3 minutes
}
export function stopAutoSync() {
clearInterval(syncInterval);
}
```
### Partner Resolution Pattern:
```javascript
// Many2one fields return [id, name] or just id
// Resolve partner names and cache them
async function resolvePartnerNames(records) {
const partnerIds = new Set();
// Collect all unique partner IDs
records.forEach(record => {
if (record.x_studio_employee) {
if (Array.isArray(record.x_studio_employee)) {
partnerIds.add(record.x_studio_employee[0]);
} else {
partnerIds.add(record.x_studio_employee);
}
}
});
// Fetch partner names in batch
const partners = await odoo.fetchPartners(Array.from(partnerIds));
const partnerMap = new Map(partners.map(p => [p.id, p.name]));
// Cache for future use
localStorage.setItem('partnerCache', JSON.stringify(
Array.from(partnerMap.entries())
));
return partnerMap;
}
```
---
## Layer 3: API Client (Odoo Communication) 🔌
### Purpose:
Abstract Odoo API communication with clean, reusable methods.
### JSON-RPC Communication:
```javascript
async function jsonRpc(url, params) {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'call',
params: params,
id: Math.random()
})
});
const data = await response.json();
if (data.error) {
throw new Error(data.error.message);
}
return data.result;
}
```
### CRUD Methods:
#### Create
```javascript
export async function createRecord(model, fields) {
return await fetch('/api/odoo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'create',
model,
fields
})
});
}
```
#### Read
```javascript
export async function searchRecords(model, domain, fields) {
return await fetch('/api/odoo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'search',
model,
domain,
fields
})
});
}
```
#### Update
```javascript
export async function updateRecord(model, id, values) {
return await fetch('/api/odoo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'update',
model,
id,
values
})
});
}
```
#### Delete
```javascript
export async function deleteRecord(model, id) {
return await fetch('/api/odoo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'delete',
model,
id
})
});
}
```
### Field Formatting Helpers:
```javascript
// Many2one: Convert to Odoo format [id, false]
export function formatMany2one(id) {
return id ? [id, false] : false;
}
// Many2many: Convert to Odoo command [(6, 0, [ids])]
export function formatMany2many(ids) {
return ids && ids.length > 0 ? [[6, 0, ids]] : [[6, 0, []]];
}
```
---
## Layer 4: Server Routes (API Proxy) 🔐
### Purpose:
Securely handle Odoo authentication and proxy requests.
### Why Server-Side?
1. **Security** - API keys never exposed to client
2. **CORS** - Bypass cross-origin restrictions
3. **Caching** - Cache UIDs to reduce auth calls
4. **Error Handling** - Centralized error management
### UID Caching Pattern:
```javascript
let cachedUid = null;
async function authenticate() {
if (cachedUid) {
return cachedUid;
}
// Authenticate with Odoo
cachedUid = await odooClient.authenticate(
db, username, apiKey
);
return cachedUid;
}
```
### Action Routing:
```javascript
export async function POST({ request }) {
const { action, model, ...params } = await request.json();
const uid = await authenticate();
switch (action) {
case 'create':
return odooClient.create(model, params.fields);
case 'search':
return odooClient.searchRead(model, params.domain, params.fields);
case 'update':
return odooClient.write(model, params.id, params.values);
case 'delete':
return odooClient.unlink(model, params.id);
default:
throw new Error(`Unknown action: ${action}`);
}
}
```
---
## Data Flow Examples 🔄
### Example 1: Loading Data on Page Load
```
1. User navigates to page
2. Component calls expenseCache.load()
3. Cache store loads from localStorage/IndexedDB
4. UI updates with cached data (instant)
5. Cache checks if data is stale (> 5 min)
6. If stale, triggers background sync
7. API client calls /api/odoo
8. Server authenticates and calls Odoo
9. New records returned
10. Cache updated (localStorage + IndexedDB)
11. UI reactively updates with new data
```
### Example 2: Creating a Record
```
1. User submits form
2. Component calls expenseCache.create(data)
3. Cache creates temp record with temp-ID
4. UI updates immediately (optimistic)
5. API client calls /api/odoo (background)
6. Server creates record in Odoo
7. Real ID returned
8. Cache replaces temp-ID with real ID
9. localStorage and IndexedDB updated
10. UI shows success message
```
### Example 3: Offline Create → Online Sync
```
1. User creates record (offline)
2. Record saved to cache with temp-ID
3. API call fails (no network)
4. Record marked as "pending sync"
5. UI shows "Will sync when online"
[User goes online]
6. Background sync detects pending records
7. Retries API call
8. Success! Real ID received
9. Cache updated
10. UI shows "Synced" status
```
---
## Key Design Patterns 🎯
### 1. Offline-First
- Always load from cache first
- Sync in background
- Queue operations when offline
- Retry on reconnection
### 2. Optimistic UI
- Update UI immediately
- Sync with server in background
- Rollback on error
- Show pending states
### 3. Incremental Sync
- Don't re-fetch all data
- Only fetch new records (id > lastRecordId)
- Reduces bandwidth
- Faster sync times
### 4. Dual Storage
- localStorage for metadata (fast)
- IndexedDB for data (large)
- Best of both worlds
### 5. Partner Resolution
- Batch fetch related records
- Cache partner names
- Avoid N+1 queries
- Display human-readable names
### 6. Reactive State
- Framework-native reactivity
- UI updates automatically
- No manual DOM manipulation
- Cleaner code
---
## Performance Considerations ⚡
### Initial Load:
- Cache-first: Instant data display
- Background sync: Fetch updates without blocking
- IndexedDB: Fast access to large datasets
### Network Usage:
- Incremental sync: Only new data
- Batch operations: Combine requests
- UID caching: Reduce auth calls
### Memory Usage:
- Store large data in IndexedDB, not memory
- Clean up old data periodically
- Lazy load related data
### Bundle Size:
- Framework-specific optimizations
- Tree shaking
- Code splitting
- Lazy load routes
---
## Security Patterns 🔒
### Credential Management:
- API keys in environment variables
- Server-side authentication
- Never expose keys to client
- Rotate keys periodically
### Data Validation:
- Validate on client (UX)
- Validate on server (security)
- Sanitize inputs
- Check permissions
### Error Handling:
- Don't expose internal errors to user
- Log errors securely
- Graceful degradation
- User-friendly messages
---
## Example prompts to use this command:
- `/architecture` - Show architecture details
- User: "How does the caching work?"
- User: "Explain the data flow"
- User: "What design patterns are used?"
## Next Steps:
After understanding the architecture:
1. Review generated code in your project
2. Read CLAUDE.md in your project for specific details
3. Customize patterns for your use case
4. Optimize for your specific needs
For more information, see `/help` or `/examples`!

597
commands/clear-cache.md Normal file
View File

@@ -0,0 +1,597 @@
Clear all cached data from your Odoo PWA application.
## What this command does:
- Clears localStorage cache
- Clears IndexedDB data
- Clears browser cache
- Resets service worker cache
- Forces fresh data fetch from Odoo
- Provides selective clearing options
## When to Use This Command
### ✅ Good Reasons:
- Data appears corrupted or inconsistent
- Testing fresh installation
- After Odoo schema changes
- Debugging sync issues
- After major updates
- Stuck with old data
### ⚠️ Caution:
- Clears all offline data
- May lose unsyncedchanges
- Requires re-download of all data
- User will need to be online
---
## Quick Clear Options
### Option 1: Clear from Browser Console (Fastest)
```javascript
// Clear localStorage
localStorage.clear();
// Clear and refresh
localStorage.clear();
location.reload();
```
### Option 2: Clear from Cache Store
```javascript
// Using cache store method
expenseCache.clearCache();
expenseCache.refresh();
```
### Option 3: Clear from UI
Add a button to your app:
```javascript
<button onclick={() => {
if (confirm('Clear all cached data?')) {
expenseCache.clearCache();
expenseCache.refresh();
}
}}>
Clear Cache
</button>
```
---
## Complete Clear Procedure
### Step 1: Prepare
```
□ Save any unsaved work
□ Ensure internet connection
□ Note current state (for comparison)
□ Close other tabs with same app
```
### Step 2: Clear localStorage
```javascript
// Clear all
localStorage.clear();
// Or clear specific keys
localStorage.removeItem('expenseCache');
localStorage.removeItem('taskCache');
localStorage.removeItem('partnerCache');
```
### Step 3: Clear IndexedDB
```javascript
// Via DevTools:
// 1. Open DevTools (F12)
// 2. Go to Application tab
// 3. Expand IndexedDB in left sidebar
// 4. Right-click on database → Delete
// Or programmatically:
indexedDB.deleteDatabase('odoo-pwa-db');
```
### Step 4: Clear Service Worker Cache
```javascript
// Unregister service worker
navigator.serviceWorker.getRegistrations()
.then(registrations => {
registrations.forEach(reg => reg.unregister());
});
// Clear all caches
caches.keys()
.then(keys => Promise.all(
keys.map(key => caches.delete(key))
));
```
### Step 5: Clear Browser Cache
```
Chrome/Edge: Ctrl+Shift+Delete → Select cache → Clear
Firefox: Ctrl+Shift+Delete → Select cache → Clear now
Safari: Cmd+Option+E
```
### Step 6: Hard Refresh
```
Windows/Linux: Ctrl+Shift+R or Ctrl+F5
Mac: Cmd+Shift+R
```
### Step 7: Verify
```javascript
// Check localStorage is empty
console.log('localStorage size:', localStorage.length);
// Check IndexedDB
// DevTools → Application → IndexedDB
// Should be empty or recreated
// Check cache stores
console.log('Records:', $expenseCache.length);
// Should be 0 or freshly fetched
```
---
## Selective Clearing
### Clear Only Specific Model Cache
```javascript
// Clear just expense cache
localStorage.removeItem('expenseCache');
// Refresh that cache
expenseCache.refresh();
```
### Clear Only Metadata (Keep Records)
```javascript
// Get current cache data
const cacheData = JSON.parse(localStorage.getItem('expenseCache'));
// Reset only metadata
cacheData.lastSyncTime = 0;
cacheData.lastRecordId = 0;
// Save back
localStorage.setItem('expenseCache', JSON.stringify(cacheData));
// Force sync
expenseCache.refresh();
```
### Clear Only Old Records
```javascript
// Keep only recent records (last 30 days)
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
expenseCache.records.update(records =>
records.filter(r => new Date(r.x_studio_date) > thirtyDaysAgo)
);
```
---
## Framework-Specific Clearing
### SvelteKit
```javascript
// Clear and reset store
import { expenseCache } from '$lib/stores/expenseCache';
expenseCache.clearCache();
expenseCache.load(); // Reload fresh data
```
### React
```javascript
// Using Context
import { useExpense } from './contexts/ExpenseContext';
function ClearCacheButton() {
const { clearCache, refresh } = useExpense();
return (
<button onClick={() => {
clearCache();
refresh();
}}>
Clear Cache
</button>
);
}
```
### Vue
```javascript
// Using Pinia store
import { useExpenseStore } from '@/stores/expenseStore';
const expenseStore = useExpenseStore();
function clearAndRefresh() {
expenseStore.clearCache();
expenseStore.load();
}
```
---
## Advanced Cache Management
### Schedule Automatic Cache Clear
```javascript
// Clear cache older than 7 days
const CACHE_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days
function checkCacheAge() {
const cacheData = JSON.parse(localStorage.getItem('expenseCache'));
if (cacheData && cacheData.lastSyncTime) {
const age = Date.now() - cacheData.lastSyncTime;
if (age > CACHE_MAX_AGE) {
console.log('Cache too old, clearing...');
expenseCache.clearCache();
expenseCache.refresh();
}
}
}
// Check on app load
checkCacheAge();
```
### Clear on Version Change
```javascript
// In cache store
const CACHE_VERSION = 2; // Increment when schema changes
function checkCacheVersion() {
const cacheData = JSON.parse(localStorage.getItem('expenseCache'));
if (!cacheData || cacheData.version !== CACHE_VERSION) {
console.log('Cache version mismatch, clearing...');
clearCache();
// Set new version
localStorage.setItem('expenseCache', JSON.stringify({
version: CACHE_VERSION,
lastSyncTime: 0,
lastRecordId: 0
}));
}
}
```
### Clear Based on Storage Quota
```javascript
// Check storage usage
if (navigator.storage && navigator.storage.estimate) {
navigator.storage.estimate().then(estimate => {
const percentUsed = (estimate.usage / estimate.quota) * 100;
console.log(`Storage: ${percentUsed.toFixed(2)}% used`);
if (percentUsed > 90) {
console.warn('Storage almost full, clearing old cache...');
clearOldestRecords();
}
});
}
function clearOldestRecords() {
// Keep only most recent 100 records
expenseCache.records.update(records =>
records
.sort((a, b) => b.id - a.id)
.slice(0, 100)
);
}
```
---
## Cache Verification
### After Clearing, Verify:
#### 1. Storage is Empty
```javascript
console.log('localStorage keys:', Object.keys(localStorage));
// Should be [] or minimal
console.log('localStorage size:', localStorage.length);
// Should be 0 or very small
```
#### 2. IndexedDB is Clear
```
DevTools → Application → IndexedDB
- Check if database exists
- Check if tables are empty
```
#### 3. Fresh Data Loads
```javascript
// Refresh and watch console
location.reload();
// Should see:
// "Cache miss, fetching from Odoo..."
// "Fetched X records"
```
#### 4. Functionality Works
```
□ Data loads correctly
□ CRUD operations work
□ Sync happens
□ Offline mode works after re-caching
```
---
## Troubleshooting Clear Issues
### Issue: Cache Won't Clear
**Solution 1: Force with DevTools**
```
1. Open DevTools (F12)
2. Application tab
3. Clear storage section
4. Check all boxes
5. Click "Clear site data"
```
**Solution 2: Use Private/Incognito**
```
Open app in private browsing mode
- Fresh session, no cache
- Test functionality
```
**Solution 3: Different Browser**
```
Test in different browser
- Rules out browser-specific issues
```
### Issue: Data Reappears After Clear
**Solution: Check Multiple Sources**
```javascript
// Clear all possible locations
localStorage.clear();
sessionStorage.clear();
indexedDB.deleteDatabase('odoo-pwa-db');
// Unregister service workers
navigator.serviceWorker.getRegistrations()
.then(regs => regs.forEach(reg => reg.unregister()));
// Clear all caches
caches.keys()
.then(keys => Promise.all(keys.map(k => caches.delete(k))));
```
### Issue: App Breaks After Clear
**Solution: Ensure Graceful Degradation**
```javascript
// In cache store, handle missing cache
function loadFromCache() {
try {
const cached = localStorage.getItem('expenseCache');
if (!cached) {
console.log('No cache, will fetch from Odoo');
return { records: [], lastRecordId: 0, lastSyncTime: 0 };
}
return JSON.parse(cached);
} catch (error) {
console.error('Error loading cache:', error);
return { records: [], lastRecordId: 0, lastSyncTime: 0 };
}
}
```
---
## User-Facing Clear Options
### Settings Page Example
```javascript
<script>
import { expenseCache, taskCache } from '$lib/stores';
async function clearAll() {
if (confirm('Clear all cached data? This cannot be undone.')) {
expenseCache.clearCache();
taskCache.clearCache();
alert('Cache cleared! Refreshing...');
location.reload();
}
}
async function clearExpenses() {
if (confirm('Clear expense cache?')) {
expenseCache.clearCache();
await expenseCache.refresh();
alert('Expense cache cleared!');
}
}
function getCacheInfo() {
const size = new Blob(Object.values(localStorage)).size;
const sizeKB = (size / 1024).toFixed(2);
return { count: localStorage.length, sizeKB };
}
</script>
<div class="settings">
<h2>Cache Management</h2>
<div class="cache-info">
<p>Items: {getCacheInfo().count}</p>
<p>Size: {getCacheInfo().sizeKB} KB</p>
<p>Last sync: {new Date($expenseCache.lastSync).toLocaleString()}</p>
</div>
<div class="actions">
<button on:click={clearExpenses}>Clear Expense Cache</button>
<button on:click={clearAll} class="danger">Clear All Cache</button>
</div>
</div>
<style>
.danger {
background: red;
color: white;
}
</style>
```
---
## Cache Clear Checklist ✅
Before clearing:
```
□ Save any unsaved work
□ Note current state
□ Ensure internet connection
□ Close duplicate tabs
```
After clearing:
```
□ localStorage is empty
□ IndexedDB is cleared
□ Service worker cache cleared
□ Fresh data loaded
□ Functionality tested
□ Sync works correctly
□ Offline mode re-enabled (after cache rebuilt)
```
---
## Best Practices
### 1. Clear Strategically
- Don't clear unnecessarily
- Clear only what's needed
- Keep user data when possible
### 2. Warn Users
```javascript
function clearCache() {
const message = `
This will clear all cached data.
You'll need to re-download everything from Odoo.
Continue?
`;
if (confirm(message)) {
// Proceed
}
}
```
### 3. Provide Progress
```javascript
async function clearAndRefresh() {
alert('Clearing cache...');
localStorage.clear();
alert('Fetching fresh data...');
await expenseCache.refresh();
alert('Done! Cache rebuilt.');
}
```
### 4. Log for Debugging
```javascript
function clearCache() {
console.log('Before clear:', {
localStorage: localStorage.length,
records: $expenseCache.length
});
localStorage.clear();
expenseCache.clearCache();
console.log('After clear:', {
localStorage: localStorage.length,
records: $expenseCache.length
});
}
```
### 5. Test Regularly
- Test cache clear functionality
- Ensure app works after clear
- Verify data re-downloads
- Check offline mode recovers
---
## Example prompts to use this command:
- `/clear-cache` - Clear all cached data
- User: "Clear my cache"
- User: "Data looks wrong, clear everything"
- User: "Reset the app"
## Related Commands:
- `/fix-sync` - If sync issues persist
- `/test-connection` - Test after clearing
- `/troubleshoot` - For other issues
- `/help` - Full documentation
---
## Quick Reference
### Clear Everything (Nuclear Option)
```javascript
// Copy-paste into console
localStorage.clear();
sessionStorage.clear();
indexedDB.databases().then(dbs =>
dbs.forEach(db => indexedDB.deleteDatabase(db.name))
);
navigator.serviceWorker.getRegistrations().then(regs =>
regs.forEach(reg => reg.unregister())
);
caches.keys().then(keys =>
keys.forEach(key => caches.delete(key))
);
location.reload(true);
```
### Clear Specific Model
```javascript
localStorage.removeItem('expenseCache');
expenseCache.refresh();
```
### Reset Metadata Only
```javascript
const cache = JSON.parse(localStorage.getItem('expenseCache'));
cache.lastSyncTime = 0;
cache.lastRecordId = 0;
localStorage.setItem('expenseCache', JSON.stringify(cache));
expenseCache.refresh();
```

View File

@@ -0,0 +1,89 @@
Create a smart cache store for an Odoo model with offline-first capabilities.
## What this command does:
- Creates a framework-specific cache store (Svelte store, React Context, or Vue Pinia)
- Implements dual storage (localStorage + IndexedDB)
- Adds smart caching with stale detection (5-minute validity)
- Implements background sync (3-minute intervals)
- Includes incremental fetching (only new records)
- Adds partner name resolution and caching
- Implements optimistic updates
## Required Information:
Before starting, gather:
1. **Current working directory** - Must be inside an Odoo PWA project
2. **Framework** - Detect from project files (SvelteKit/React/Vue)
3. **Model name** (without `x_` prefix, e.g., "expense", "task")
4. **Model display name** (human-readable, e.g., "Expense", "Task")
5. **Fields to fetch** - Array of Odoo fields (e.g., ["x_studio_name", "x_studio_amount"])
## Store Features:
The generated cache store will include:
- `records` - Reactive array of cached records
- `isLoading` - Loading state indicator
- `error` - Error state
- `lastSync` - Timestamp of last successful sync
- `load()` - Load from cache and trigger background sync
- `create(data)` - Create new record with optimistic update
- `update(id, data)` - Update record with optimistic update
- `delete(id)` - Delete record with optimistic update
- `refresh()` - Force refresh from Odoo
- `clearCache()` - Clear all cached data
## Storage Strategy:
1. **localStorage**: Stores metadata (lastSyncTime, lastRecordId, version)
2. **IndexedDB**: Stores master data (all records)
3. **Stale detection**: Cache considered stale after 5 minutes
4. **Background sync**: Automatic sync every 3 minutes
5. **Incremental fetch**: Only fetches records with `id > lastRecordId`
## Steps:
1. Verify the current directory is an Odoo PWA project
2. Detect the framework from project structure
3. Ask the user for model details and fields
4. Create cache store file in appropriate location:
- SvelteKit: `src/lib/stores/{model}Cache.js`
- React: `src/contexts/{Model}Context.jsx`
- Vue: `src/stores/{model}Store.js`
5. Import and register the store in the appropriate location
6. Provide usage examples and documentation
## Example prompts to use this command:
- `/create-cache-store` - Interactive mode
- User: "Create a cache store for the task model"
- User: "Add caching for product catalog"
## After creation:
Remind the user to:
1. Import the store in components that need it
2. Call the `load()` method when the component mounts
3. Use reactive data bindings to display records
4. Test create, update, and delete operations
5. Verify offline functionality works correctly
## Usage Examples:
### SvelteKit
```javascript
import { taskCache } from '$lib/stores/taskCache';
// In +page.svelte
$effect(() => {
taskCache.load();
});
```
### React
```javascript
import { useTask } from './contexts/TaskContext';
const { records, create, update } = useTask();
```
### Vue
```javascript
import { useTaskStore } from '@/stores/taskStore';
const taskStore = useTaskStore();
taskStore.load();
```

182
commands/deploy-github.md Normal file
View File

@@ -0,0 +1,182 @@
Deploy your Odoo PWA to GitHub Pages with GitHub Actions for continuous deployment.
## What this command does:
- Sets up GitHub Actions workflow for automated deployment
- Configures GitHub Pages in repository settings
- Sets up repository secrets for environment variables
- Deploys the application to GitHub Pages
- Provides custom domain setup instructions
## Prerequisites:
Before deploying, verify:
1. ✅ Project builds successfully locally (`npm run build`)
2. ✅ Git repository exists and is pushed to GitHub
3. ✅ User has admin access to the repository
4. ✅ GitHub Pages is enabled in repository settings
5. ✅ Base URL configuration is correct for GitHub Pages
## Important: GitHub Pages Limitations
⚠️ Note: GitHub Pages is static hosting only. Server-side API routes won't work.
**Recommendation**: For full Odoo PWA functionality with server-side API proxy:
- Use Vercel, Cloudflare Pages, or Netlify instead
- Or deploy API routes separately (e.g., Vercel Serverless Functions)
## If Continuing with GitHub Pages:
You'll need to modify the Odoo client to use CORS-enabled direct Odoo API calls or deploy API routes separately.
## Steps:
### 1. Configure GitHub Repository
```bash
# Ensure you're on main branch
git checkout main
git pull origin main
```
### 2. Set Up GitHub Actions Workflow
Check if `.github/workflows/deploy.yml` exists:
- If yes: Review and update if needed
- If no: Create the workflow file
### 3. Configure Repository Secrets
Go to: Repository → Settings → Secrets and variables → Actions
Add these secrets:
```
ODOO_API_KEY=your_production_api_key
ODOO_USERNAME=your.email@company.com
VITE_ODOO_URL=https://yourcompany.odoo.com
VITE_ODOO_DB=yourcompany-main
VITE_MODEL_NAME=x_expense
VITE_MODEL_DISPLAY_NAME=Expense
```
### 4. Enable GitHub Pages
Repository → Settings → Pages:
- Source: Deploy from a branch
- Branch: `gh-pages` (will be created by Actions)
- Folder: `/ (root)`
### 5. Update Base Path
For framework-specific configuration:
**SvelteKit** (`svelte.config.js`):
```javascript
paths: {
base: process.env.NODE_ENV === 'production' ? '/your-repo-name' : ''
}
```
**React/Vue** (`vite.config.js`):
```javascript
base: process.env.NODE_ENV === 'production' ? '/your-repo-name/' : '/'
```
### 6. Commit and Push
```bash
git add .
git commit -m "Configure GitHub Pages deployment"
git push origin main
```
### 7. Monitor Deployment
- Go to Actions tab in GitHub
- Watch the deployment workflow
- Check for any errors
## GitHub Actions Workflow
The workflow should:
1. Trigger on push to `main` branch
2. Install dependencies
3. Build the project with environment variables
4. Deploy to `gh-pages` branch
5. GitHub Pages automatically serves from `gh-pages`
## Example Workflow (.github/workflows/deploy.yml):
```yaml
name: Deploy to GitHub Pages
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install
- run: npm run build
env:
VITE_ODOO_URL: ${{ secrets.VITE_ODOO_URL }}
VITE_ODOO_DB: ${{ secrets.VITE_ODOO_DB }}
VITE_MODEL_NAME: ${{ secrets.VITE_MODEL_NAME }}
VITE_MODEL_DISPLAY_NAME: ${{ secrets.VITE_MODEL_DISPLAY_NAME }}
- uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./build
```
## Custom Domain Setup (Optional):
1. Add `CNAME` file to `static` folder with your domain
2. Configure DNS records:
- A record: Points to GitHub Pages IPs
- Or CNAME: Points to `username.github.io`
3. Enable HTTPS in repository settings (automatic)
## Post-Deployment Checks:
After deployment, verify:
1. ✅ Application loads at `https://username.github.io/repo-name`
2. ✅ All assets load correctly (check browser console)
3. ✅ Base path is correct for all routes
4. ✅ Odoo connection works (may need CORS configuration)
5. ✅ PWA installs correctly
## Example prompts to use this command:
- `/deploy-github` - Interactive GitHub Pages deployment
- User: "Deploy to GitHub Pages"
- User: "Set up GitHub Actions for my PWA"
## Troubleshooting:
### Build Fails in Actions
- Check Actions logs for specific error
- Verify all secrets are set correctly
- Ensure Node version is compatible
- Test build locally first
### 404 on GitHub Pages
- Verify `gh-pages` branch exists
- Check GitHub Pages settings
- Ensure base path is configured
- Wait a few minutes for DNS propagation
### Assets Not Loading
- Check base path configuration
- Verify all asset paths are relative
- Look for CORS issues in console
- Ensure service worker paths are correct
### API Routes Don't Work
- GitHub Pages doesn't support server-side code
- Deploy API routes to Vercel/Netlify separately
- Or modify Odoo client for direct API calls with CORS
## After Deployment:
Provide the user with:
1. GitHub Pages URL
2. Link to Actions tab for monitoring
3. Instructions for custom domain setup
4. Reminder about server-side limitations
5. Alternative hosting recommendations if needed

144
commands/deploy-vercel.md Normal file
View File

@@ -0,0 +1,144 @@
Deploy your Odoo PWA to Vercel with proper environment variable configuration.
## What this command does:
- Prepares the project for Vercel deployment
- Guides through Vercel CLI setup or web deployment
- Configures environment variables securely
- Sets up continuous deployment from Git
- Provides post-deployment verification steps
## Prerequisites:
Before deploying, verify:
1. ✅ Project builds successfully locally (`npm run build`)
2. ✅ All tests pass
3.`.env` file is configured and working
4. ✅ Git repository is initialized and pushed to GitHub/GitLab/Bitbucket
5. ✅ Vercel account exists (or guide user to create one)
## Deployment Options:
### Option 1: Vercel CLI (Recommended for first deployment)
```bash
npm install -g vercel
vercel login
vercel
```
### Option 2: Vercel Dashboard (Recommended for Git integration)
1. Go to https://vercel.com/new
2. Import your Git repository
3. Configure project settings
4. Add environment variables
5. Deploy
## Environment Variables to Set in Vercel:
Required for production:
```
VITE_ODOO_URL=https://yourcompany.odoo.com
VITE_ODOO_DB=yourcompany-main
ODOO_API_KEY=your_production_api_key
ODOO_USERNAME=your.email@company.com
VITE_MODEL_NAME=x_expense
VITE_MODEL_DISPLAY_NAME=Expense
```
## Steps:
1. Verify project builds successfully
2. Check if Vercel is already configured (look for `vercel.json`)
3. Ask user which deployment option they prefer
4. Guide through the chosen deployment method
5. Help set up environment variables in Vercel dashboard
6. Initiate deployment
7. Wait for build to complete
8. Test the deployed application
9. Set up custom domain (if requested)
## Framework-Specific Configuration:
### SvelteKit
Verify `vercel.json` contains:
```json
{
"buildCommand": "npm run build",
"outputDirectory": "build",
"framework": "sveltekit"
}
```
### React
Verify build settings:
```json
{
"buildCommand": "npm run build",
"outputDirectory": "dist",
"framework": "vite"
}
```
### Vue
Verify build settings:
```json
{
"buildCommand": "npm run build",
"outputDirectory": "dist",
"framework": "vite"
}
```
## Post-Deployment Checks:
After deployment, verify:
1. ✅ Application loads correctly
2. ✅ Odoo connection works (check browser console)
3. ✅ Data syncs from Odoo
4. ✅ CRUD operations work
5. ✅ Offline functionality works
6. ✅ PWA can be installed
7. ✅ Service worker is active
## Example prompts to use this command:
- `/deploy-vercel` - Interactive deployment wizard
- User: "Deploy my PWA to Vercel"
- User: "Help me set up Vercel deployment"
## Continuous Deployment:
Once Git integration is set up:
- Every push to `main` branch triggers automatic deployment
- Preview deployments for pull requests
- Automatic rollback on build failures
- Environment variables persist across deployments
## Custom Domain Setup:
If user wants a custom domain:
1. Go to Vercel Dashboard → Project → Settings → Domains
2. Add custom domain
3. Configure DNS records as shown
4. Wait for SSL certificate provisioning (automatic)
5. Test HTTPS access
## Troubleshooting:
### Build Fails
- Check build logs in Vercel dashboard
- Verify all dependencies are in package.json
- Ensure Node version is compatible
- Check for environment-specific code
### Environment Variables Not Working
- Verify variables are set in Vercel dashboard
- Check variable names match exactly (case-sensitive)
- Ensure variables starting with `VITE_` for client-side access
- Redeploy after adding new variables
### API Routes Not Working
- Verify serverless functions are in correct directory
- Check function size limits (< 50MB)
- Review function logs in Vercel dashboard
- Ensure API routes use correct paths
## After Deployment:
Provide the user with:
1. Deployed URL
2. Vercel dashboard link
3. Instructions for adding custom domain
4. Tips for monitoring performance
5. Reminder to update API keys for production

467
commands/examples.md Normal file
View File

@@ -0,0 +1,467 @@
Real-world usage examples and scenarios for the Odoo PWA Generator plugin.
## What this command does:
- Provides practical, real-world examples of using the plugin
- Shows complete workflows from start to finish
- Demonstrates different use cases and scenarios
- Includes code samples and best practices
- Helps users understand the plugin's capabilities
## Example 1: Expense Tracking App 💰
### Business Need:
Create a mobile-friendly app for employees to track expenses on-the-go, even without internet connection. Sync with Odoo for approval and reimbursement.
### Odoo Model Setup:
In Odoo Studio, create model `x_expense` with fields:
- `x_studio_description` (Char) - Expense description
- `x_studio_amount` (Float) - Amount spent
- `x_studio_date` (Date) - When expense occurred
- `x_studio_category` (Selection) - Meal, Travel, Hotel, Other
- `x_studio_receipt` (Binary) - Photo of receipt
- `x_studio_employee` (Many2one to res.partner) - Who spent it
- `x_studio_status` (Selection) - Draft, Submitted, Approved, Paid
### Implementation Steps:
```
1. /new-svelte-pwa
- Project name: expense-tracker
- Model: expense
- Display name: Expense
- Deployment: vercel
2. cd expense-tracker
3. /init-project
- Install dependencies
- Configure Odoo credentials
- Test connection
4. Customize the UI:
- Add category filter
- Display total amount
- Add receipt upload
- Status badge colors
5. /deploy-vercel
- Deploy to production
- Share with team
```
### Key Features:
- ✅ Record expenses offline
- ✅ Take photos of receipts
- ✅ Categorize expenses
- ✅ Auto-sync when online
- ✅ View approval status
- ✅ Calculate monthly totals
### Code Customization:
```javascript
// Add total calculation to cache store
export const totalExpenses = derived(expenseCache, $cache => {
return $cache.reduce((sum, exp) => sum + exp.x_studio_amount, 0);
});
// Add category filter
export function filterByCategory(category) {
return $expenseCache.filter(e => e.x_studio_category === category);
}
```
---
## Example 2: Inventory Management System 📦
### Business Need:
Warehouse staff need to check stock levels, update quantities, and add new inventory items from mobile devices or tablets, even in areas with poor connectivity.
### Odoo Model Setup:
Create model `x_inventory` with fields:
- `x_studio_sku` (Char) - Product SKU
- `x_studio_name` (Char) - Product name
- `x_studio_quantity` (Integer) - Current stock
- `x_studio_location` (Char) - Warehouse location
- `x_studio_min_quantity` (Integer) - Reorder threshold
- `x_studio_supplier` (Many2one to res.partner) - Supplier
- `x_studio_last_restock` (Date) - Last restock date
### Implementation Steps:
```
1. /new-react-pwa
- Project name: inventory-manager
- Model: inventory
- Display name: Inventory Item
- Deployment: vercel
2. cd inventory-manager
3. /init-project
4. Add barcode scanning:
- npm install @zxing/library
- Add scanner component
- Look up items by SKU
5. Add low stock alerts:
- Filter items where quantity < min_quantity
- Show notification badge
- Sort by urgency
6. /deploy-vercel
```
### Key Features:
- ✅ Scan barcodes to find items
- ✅ Update quantities offline
- ✅ Low stock alerts
- ✅ Search by name or SKU
- ✅ Filter by location
- ✅ Auto-sync updates
### Code Customization:
```javascript
// Add low stock filter
const lowStockItems = useMemo(() => {
return records.filter(item =>
item.x_studio_quantity < item.x_studio_min_quantity
);
}, [records]);
// Add barcode lookup
async function lookupBySKU(sku) {
return records.find(item => item.x_studio_sku === sku);
}
```
---
## Example 3: Field Service CRM 🔧
### Business Need:
Field technicians need to view customer information, log service calls, and update job status while on-site, often without reliable internet.
### Odoo Model Setup:
Create model `x_service_call` with fields:
- `x_studio_customer` (Many2one to res.partner) - Customer
- `x_studio_issue` (Text) - Problem description
- `x_studio_status` (Selection) - Scheduled, In Progress, Completed
- `x_studio_scheduled_date` (Datetime) - When to visit
- `x_studio_technician` (Many2one to res.partner) - Assigned tech
- `x_studio_notes` (Text) - Service notes
- `x_studio_parts_used` (Char) - Parts replaced
- `x_studio_duration` (Float) - Hours spent
### Implementation Steps:
```
1. /new-vue-pwa
- Project name: field-service-crm
- Model: service_call
- Display name: Service Call
- Deployment: vercel
2. /init-project
3. Add customer details:
- Create customer cache store
- /add-model
- Model: customer (use res.partner)
- Generate UI: no (use existing)
4. Add map integration:
- npm install @googlemaps/js-api-loader
- Show customer locations
- Route planning
5. Add time tracking:
- Start/stop timer
- Calculate duration
- Generate timesheet
6. /deploy-vercel
```
### Key Features:
- ✅ View today's schedule
- ✅ Customer contact info
- ✅ Log service notes offline
- ✅ Track time spent
- ✅ Update job status
- ✅ View service history
---
## Example 4: Sales Order Entry 🛒
### Business Need:
Sales reps at trade shows need to take orders offline and sync them with Odoo when they get back online.
### Odoo Model Setup:
Create model `x_sales_order` with fields:
- `x_studio_customer` (Many2one to res.partner)
- `x_studio_date` (Date)
- `x_studio_items` (Text/JSON) - Line items
- `x_studio_total` (Float) - Order total
- `x_studio_status` (Selection) - Draft, Sent, Confirmed
- `x_studio_notes` (Text) - Special instructions
- `x_studio_salesperson` (Many2one to res.partner)
### Implementation Steps:
```
1. /new-svelte-pwa
- Project name: sales-order-entry
- Model: sales_order
- Display name: Sales Order
2. /add-model
- Model: customer (res.partner)
- Add product catalog model
3. Build line item editor:
- Add/remove products
- Quantity and price
- Calculate totals
4. Add customer search:
- Autocomplete
- Recently viewed
- New customer form
5. /deploy-vercel
```
### Key Features:
- ✅ Search products
- ✅ Build order offline
- ✅ Calculate totals
- ✅ Customer lookup
- ✅ Sync when online
- ✅ Email confirmation
---
## Example 5: Multi-Model Project Management 📋
### Business Need:
Manage projects with tasks, time entries, and documents, all syncing with Odoo.
### Multiple Models:
1. `x_project` - Projects
2. `x_task` - Tasks
3. `x_time_entry` - Time tracking
4. `x_document` - File attachments
### Implementation Steps:
```
1. /new-svelte-pwa
- Project name: project-manager
- Model: project
- Display name: Project
2. /add-model
- Model: task
- Generate UI: yes
3. /add-model
- Model: time_entry
- Generate UI: yes
4. /add-model
- Model: document
- Generate UI: yes
5. Add relationships:
- Tasks belong to projects
- Time entries belong to tasks
- Documents belong to projects
6. Build dashboard:
- Project overview
- Task list by status
- Total hours tracked
- Recent documents
7. /deploy-vercel
```
### Key Features:
- ✅ Multiple model types
- ✅ Relationships between models
- ✅ Aggregate data (total hours)
- ✅ Complex filtering
- ✅ Dashboard views
---
## Example 6: Custom Cache Strategy 🎯
### Scenario:
Need a custom caching strategy for frequently changing data.
### Implementation:
```
1. Open existing project
2. /create-cache-store
- Model: notification
- Shorter cache timeout (1 minute)
- More frequent sync (30 seconds)
3. Customize the generated store:
```
```javascript
// Shorten cache validity
const CACHE_VALIDITY = 60 * 1000; // 1 minute
// More frequent sync
const SYNC_INTERVAL = 30 * 1000; // 30 seconds
// Add real-time refresh
export function enableRealTimeSync() {
return setInterval(() => {
refresh();
}, SYNC_INTERVAL);
}
```
---
## Example 7: Migrating Existing App 🔄
### Scenario:
Have an existing web app, want to add Odoo integration and offline functionality.
### Implementation Steps:
```
1. Generate reference implementation:
/new-svelte-pwa
- Project name: reference-app
- Model: your_model
2. Study generated code:
- Review odoo.js client
- Study cache.js pattern
- Examine API routes
3. Copy patterns to existing app:
- Copy src/lib/odoo.js
- Copy src/routes/api/odoo/+server.js
- Adapt cache store to your state management
4. Test integration:
/test-connection
5. Gradually add features:
- Start with read-only
- Add create functionality
- Add update/delete
- Add offline support
6. /deploy-vercel
```
---
## Common Customizations:
### 1. Add Search Functionality
```javascript
export function searchRecords(query) {
return $cache.filter(record =>
record.x_studio_name.toLowerCase().includes(query.toLowerCase())
);
}
```
### 2. Add Sorting
```javascript
export function sortBy(field, direction = 'asc') {
return $cache.sort((a, b) => {
const valA = a[field];
const valB = b[field];
return direction === 'asc' ? valA - valB : valB - valA;
});
}
```
### 3. Add Pagination
```javascript
export function paginate(page, pageSize) {
const start = (page - 1) * pageSize;
return $cache.slice(start, start + pageSize);
}
```
### 4. Add Export to CSV
```javascript
export function exportToCSV() {
const headers = ['ID', 'Name', 'Amount', 'Date'];
const rows = $cache.map(r => [
r.id,
r.x_studio_name,
r.x_studio_amount,
r.x_studio_date
]);
// Convert to CSV and download
}
```
### 5. Add Bulk Operations
```javascript
export async function bulkUpdate(ids, fields) {
const promises = ids.map(id => update(id, fields));
return Promise.all(promises);
}
```
---
## Tips for Success:
### Start Simple
1. Generate basic PWA first
2. Test with sample data
3. Add features incrementally
4. Deploy early and often
### Plan Your Models
1. Design Odoo model schema carefully
2. Include all necessary fields
3. Think about relationships
4. Consider mobile UX
### Test Thoroughly
1. Test offline functionality
2. Verify sync works correctly
3. Check error handling
4. Test on real devices
### Optimize Performance
1. Limit initial data load
2. Use pagination for large datasets
3. Lazy load images
4. Minimize bundle size
### Deploy Confidently
1. Test build locally
2. Use staging environment
3. Monitor errors
4. Have rollback plan
---
## Example prompts to use this command:
- `/examples` - Show all examples
- User: "Show me real-world examples"
- User: "How do I build an expense tracker?"
- User: "Give me ideas for using this plugin"
## Next Steps:
After reviewing these examples:
1. Choose a use case similar to your needs
2. Follow the implementation steps
3. Customize to match your requirements
4. Deploy and iterate
Need help? Run `/help` for more information!

596
commands/fix-sync.md Normal file
View File

@@ -0,0 +1,596 @@
Diagnose and fix synchronization issues between your PWA and Odoo.
## What this command does:
- Identifies sync problems
- Tests each component of the sync system
- Provides step-by-step fixes
- Clears problematic cached data
- Verifies sync works correctly
---
## Quick Diagnosis 🔍
Run through these quick checks first:
### 1. Visual Inspection
```
□ Check browser console for errors
□ Look at "Last synced" timestamp
□ Try manual refresh
□ Check network tab for failed requests
```
### 2. Quick Tests
```javascript
// In browser console:
// 1. Check if cache exists
localStorage.getItem('expenseCache');
// 2. Test API endpoint
fetch('/api/odoo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'search',
model: 'res.partner',
domain: [],
fields: ['name'],
limit: 1
})
}).then(r => r.json()).then(console.log);
// 3. Force refresh
expenseCache.refresh();
```
### 3. Automatic Diagnosis
```bash
/test-connection
```
---
## Common Sync Issues
### Issue 1: Initial Load Works, But No Updates 🔄
**Symptoms:**
- Data loads on first visit
- But never updates with new Odoo data
- "Last synced" timestamp is old
- Background sync not happening
**Diagnosis:**
```javascript
// Check if sync timer is running
// In cache store, look for:
setInterval(() => syncInBackground(), 180000);
// Check if stale detection works
const cacheData = JSON.parse(localStorage.getItem('expenseCache'));
console.log('Last sync:', new Date(cacheData?.lastSyncTime));
console.log('Is stale?', Date.now() - cacheData?.lastSyncTime > 5 * 60 * 1000);
```
**Solutions:**
#### Solution A: Restart Background Sync
```javascript
// In browser console
expenseCache.stopAutoSync();
expenseCache.startAutoSync();
```
#### Solution B: Check Cache Validity
```javascript
// In cache store file (e.g., expenseCache.js)
const CACHE_VALIDITY = 5 * 60 * 1000; // 5 minutes
function isCacheStale() {
const cacheData = JSON.parse(localStorage.getItem('expenseCache'));
if (!cacheData) return true;
const now = Date.now();
const age = now - cacheData.lastSyncTime;
console.log(`Cache age: ${Math.floor(age / 1000)}s`);
return age > CACHE_VALIDITY;
}
```
#### Solution C: Force Manual Sync
```javascript
// Add a refresh button to your UI
<button onclick={() => expenseCache.refresh()}>
Sync Now
</button>
```
---
### Issue 2: Incremental Sync Not Working 📈
**Symptoms:**
- Always fetches all records
- Slow sync times
- High bandwidth usage
- `lastRecordId` not updating
**Diagnosis:**
```javascript
// Check lastRecordId
const cacheData = JSON.parse(localStorage.getItem('expenseCache'));
console.log('Last record ID:', cacheData?.lastRecordId);
// Check if it's being updated after sync
```
**Solutions:**
#### Solution A: Verify Domain Filter
```javascript
// In syncInBackground() function
async function syncInBackground() {
const { lastRecordId } = getCacheMetadata();
console.log('Fetching records with id >', lastRecordId);
const domain = lastRecordId > 0
? [['id', '>', lastRecordId]]
: [];
const newRecords = await odoo.searchRecords(
MODEL_NAME,
domain,
fields
);
console.log('Fetched:', newRecords.length, 'new records');
if (newRecords.length > 0) {
// Update lastRecordId
const maxId = Math.max(...newRecords.map(r => r.id));
updateCacheMetadata({ lastRecordId: maxId });
}
}
```
#### Solution B: Reset lastRecordId
```javascript
// If stuck, reset to fetch all
const cacheData = JSON.parse(localStorage.getItem('expenseCache'));
cacheData.lastRecordId = 0;
localStorage.setItem('expenseCache', JSON.stringify(cacheData));
// Then refresh
expenseCache.refresh();
```
---
### Issue 3: Optimistic Updates Not Syncing ⚡
**Symptoms:**
- Create/update works in UI
- But changes don't save to Odoo
- Records disappear on refresh
- Temp IDs persist
**Diagnosis:**
```javascript
// Check for temp IDs
console.log($expenseCache.filter(e => e.id.toString().startsWith('temp-')));
// Check browser console for API errors
```
**Solutions:**
#### Solution A: Check API Route
```javascript
// Test create
fetch('/api/odoo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'create',
model: 'x_expense',
fields: {
x_studio_description: 'Test',
x_studio_amount: 10.00
}
})
})
.then(r => r.json())
.then(console.log)
.catch(console.error);
```
#### Solution B: Fix Error Handling
```javascript
// In cache store create() function
export async function create(data) {
const tempId = `temp-${Date.now()}`;
const tempRecord = { id: tempId, ...data };
// Add to cache
records.update(r => [...r, tempRecord]);
try {
// Create in Odoo
const realId = await odoo.createRecord(MODEL_NAME, data);
// Replace temp ID
records.update(r =>
r.map(rec => rec.id === tempId ? { ...rec, id: realId } : rec)
);
// Update cache metadata
await refresh(); // Sync to get the complete record
return realId;
} catch (error) {
console.error('Failed to create record:', error);
// Rollback
records.update(r => r.filter(rec => rec.id !== tempId));
// Re-throw
throw error;
}
}
```
#### Solution C: Clean Up Temp Records
```javascript
// Remove stuck temp records
expenseCache.records.update(records =>
records.filter(r => !r.id.toString().startsWith('temp-'))
);
// Then refresh
expenseCache.refresh();
```
---
### Issue 4: Partner Names Not Resolving 👥
**Symptoms:**
- Partner fields show IDs instead of names
- Many2one fields display as arrays
- "undefined" or "[object Object]" displayed
**Diagnosis:**
```javascript
// Check partner field format
const record = $expenseCache[0];
console.log('Employee field:', record.x_studio_employee);
// Should be: [12, "John Doe"] or 12
```
**Solutions:**
#### Solution A: Add Partner Resolution
```javascript
// In syncInBackground()
async function syncInBackground() {
// ... fetch records ...
// Resolve partner names
const partnerIds = new Set();
records.forEach(record => {
if (record.x_studio_employee) {
const id = Array.isArray(record.x_studio_employee)
? record.x_studio_employee[0]
: record.x_studio_employee;
partnerIds.add(id);
}
});
if (partnerIds.size > 0) {
const partners = await odoo.fetchPartners(Array.from(partnerIds));
const partnerMap = new Map(partners.map(p => [p.id, p.name]));
// Cache partners
localStorage.setItem('partnerCache', JSON.stringify(
Array.from(partnerMap.entries())
));
// Update records with names
records = records.map(record => {
if (record.x_studio_employee) {
const id = Array.isArray(record.x_studio_employee)
? record.x_studio_employee[0]
: record.x_studio_employee;
return {
...record,
x_studio_employee: [id, partnerMap.get(id) || 'Unknown']
};
}
return record;
});
}
}
```
#### Solution B: Display Helper Function
```javascript
// Helper to display partner name
function getPartnerName(field) {
if (!field) return 'None';
if (Array.isArray(field)) return field[1] || `ID: ${field[0]}`;
return `ID: ${field}`;
}
// In component
{getPartnerName(expense.x_studio_employee)}
```
---
### Issue 5: Duplicate Records 📋📋
**Symptoms:**
- Same record appears multiple times
- ID conflicts
- Sync creates duplicates
**Diagnosis:**
```javascript
// Check for duplicates
const ids = $expenseCache.map(r => r.id);
const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index);
console.log('Duplicate IDs:', duplicates);
```
**Solutions:**
#### Solution A: Deduplicate Cache
```javascript
function deduplicateCache() {
records.update(currentRecords => {
const seen = new Set();
return currentRecords.filter(record => {
if (seen.has(record.id)) {
return false;
}
seen.add(record.id);
return true;
});
});
}
// Run deduplication
deduplicateCache();
```
#### Solution B: Fix Sync Logic
```javascript
// When appending new records, check for duplicates
async function appendToCache(newRecords) {
records.update(currentRecords => {
const existingIds = new Set(currentRecords.map(r => r.id));
// Only add records that don't exist
const toAdd = newRecords.filter(r => !existingIds.has(r.id));
return [...currentRecords, ...toAdd];
});
}
```
---
### Issue 6: Offline Queue Not Syncing 📴
**Symptoms:**
- Changes made offline
- Online now, but changes not syncing
- Stuck in "pending" state
**Solutions:**
#### Solution A: Implement Offline Queue
```javascript
// Store failed operations
const offlineQueue = writable([]);
async function queueOperation(operation) {
offlineQueue.update(q => [...q, operation]);
saveOfflineQueue();
}
async function processOfflineQueue() {
const queue = get(offlineQueue);
for (const operation of queue) {
try {
if (operation.type === 'create') {
await odoo.createRecord(operation.model, operation.data);
} else if (operation.type === 'update') {
await odoo.updateRecord(operation.model, operation.id, operation.data);
} else if (operation.type === 'delete') {
await odoo.deleteRecord(operation.model, operation.id);
}
// Remove from queue
offlineQueue.update(q => q.filter(op => op !== operation));
} catch (error) {
console.error('Failed to process queued operation:', error);
// Keep in queue, will retry
}
}
saveOfflineQueue();
}
// Listen for online event
window.addEventListener('online', () => {
console.log('Back online, processing queue...');
processOfflineQueue();
});
```
---
## Step-by-Step Fix Procedure 🔧
### Step 1: Clear Everything
```javascript
// Clear all caches
localStorage.clear();
// Clear IndexedDB
// DevTools → Application → IndexedDB → Delete database
```
### Step 2: Test API
```bash
/test-connection
```
### Step 3: Fresh Sync
```javascript
// Refresh page
location.reload();
// Should fetch all data fresh
```
### Step 4: Monitor Sync
```javascript
// Watch console for sync messages
// Should see:
// "Syncing x_expense..."
// "Fetched X records"
// "Cache updated"
```
### Step 5: Test CRUD
```javascript
// Test create
await expenseCache.create({ /* data */ });
// Test update
await expenseCache.update(id, { /* data */ });
// Test delete
await expenseCache.remove(id);
// Verify in Odoo
```
---
## Sync Debugging Checklist ✅
```
□ Browser console shows no errors
□ /api/odoo endpoint responds
□ ODOO_API_KEY is valid
□ lastSyncTime is updating
□ lastRecordId is updating
□ Background sync interval is running
□ Stale detection works correctly
□ Incremental fetch has correct domain
□ Optimistic updates resolve temp IDs
□ Partner names resolve correctly
□ No duplicate records
□ Offline queue processes when online
□ IndexedDB is storing data
□ localStorage has metadata
```
---
## Advanced Debugging 🐛
### Enable Verbose Logging
```javascript
// Add to cache store
const DEBUG = true;
function log(...args) {
if (DEBUG) console.log('[ExpenseCache]', ...args);
}
async function syncInBackground() {
log('Starting background sync...');
log('lastRecordId:', getLastRecordId());
const records = await fetch();
log('Fetched records:', records.length);
await saveToCache(records);
log('Cache updated');
}
```
### Monitor Network
```javascript
// Log all API calls
const originalFetch = window.fetch;
window.fetch = async (...args) => {
console.log('Fetch:', args[0]);
const response = await originalFetch(...args);
console.log('Response:', response.status);
return response;
};
```
### Profile Performance
```javascript
// Measure sync time
console.time('sync');
await expenseCache.refresh();
console.timeEnd('sync');
```
---
## Example prompts to use this command:
- `/fix-sync` - Diagnose sync issues
- User: "Data is not syncing"
- User: "My changes aren't saving"
- User: "Sync is broken"
## Related Commands:
- `/test-connection` - Test Odoo connectivity
- `/clear-cache` - Clear all cached data
- `/troubleshoot` - General troubleshooting
- `/help` - Full documentation
---
## Prevention Tips 🛡️
1. **Monitor sync health**
- Display "Last synced" timestamp in UI
- Show sync status indicator
- Alert on sync failures
2. **Handle errors gracefully**
- Catch and log all errors
- Show user-friendly messages
- Provide retry mechanisms
3. **Test offline scenarios**
- Test creating records offline
- Test going offline mid-sync
- Test coming back online
4. **Keep sync simple**
- Stick to generated patterns
- Don't overcomplicate logic
- Follow proven examples
5. **Regular maintenance**
- Clear old data periodically
- Update dependencies
- Monitor performance

322
commands/help.md Normal file
View File

@@ -0,0 +1,322 @@
Display comprehensive help and documentation for the Odoo PWA Generator plugin.
## What this command does:
- Provides overview of the plugin and its capabilities
- Lists all available commands with descriptions
- Explains the plugin's skills and when to use them
- Shows common usage patterns and workflows
- Links to detailed documentation
## Plugin Overview:
The **odoo-pwa-generator** plugin helps you create offline-first Progressive Web Apps with Odoo Studio backend integration. It supports SvelteKit, React, and Vue frameworks.
### Key Features:
**Quick Project Generation** - Create complete PWAs in minutes
📱 **Offline-First** - Smart caching with background sync
🔄 **Odoo Integration** - Seamless Odoo Studio connectivity
🚀 **Production Ready** - Pre-configured deployment setups
**Framework Support** - SvelteKit, React, and Vue
### Core Capabilities:
- Automatic CRUD operations for Odoo models
- Smart caching with localStorage + IndexedDB
- Incremental sync (only fetch new records)
- Optimistic UI updates
- Partner/relation resolution
- PWA installability
- Offline functionality
## Available Commands:
### 🎯 Skill Shortcuts (Project Generation)
Create new PWA projects:
- `/new-svelte-pwa` - Generate SvelteKit PWA
- `/new-react-pwa` - Generate React PWA
- `/new-vue-pwa` - Generate Vue PWA
- `/add-model` - Add Odoo model to existing PWA
- `/create-cache-store` - Create cache store for a model
### 🔧 Workflow Commands
Development and deployment:
- `/init-project` - Initialize new PWA project
- `/setup-env` - Configure environment variables
- `/test-connection` - Test Odoo API connection
- `/deploy-vercel` - Deploy to Vercel
- `/deploy-github` - Deploy to GitHub Pages
### 📚 Documentation Commands
Help and reference:
- `/help` - This help document (you are here!)
- `/examples` - Real-world usage examples
- `/architecture` - Explain PWA architecture
- `/api-reference` - Odoo API client documentation
- `/troubleshoot` - Common issues and solutions
### 🛠 Maintenance Commands
Project management:
- `/update-deps` - Update project dependencies
- `/fix-sync` - Diagnose and fix sync issues
- `/clear-cache` - Clear application caches
- `/add-deployment` - Add new deployment target
- `/optimize` - Run optimization checks
## Common Workflows:
### 1⃣ Starting a New Project
```
1. /new-svelte-pwa (or /new-react-pwa or /new-vue-pwa)
2. Provide project details (name, model, etc.)
3. /init-project
4. Start coding!
```
### 2⃣ Setting Up Environment
```
1. /setup-env
2. Provide Odoo credentials
3. /test-connection
4. Verify everything works
```
### 3⃣ Adding More Models
```
1. /add-model
2. Specify model name and details
3. Test CRUD operations
4. Customize UI as needed
```
### 4⃣ Deploying to Production
```
1. /deploy-vercel (recommended)
OR /deploy-github
2. Configure environment variables
3. Test deployed application
4. Set up custom domain (optional)
```
### 5⃣ Troubleshooting Issues
```
1. /test-connection - Check Odoo connectivity
2. /troubleshoot - Find specific solutions
3. /fix-sync - Diagnose sync problems
4. /clear-cache - Reset if needed
```
## Quick Start Guide:
### Step 1: Generate a New PWA
Choose your framework and run the appropriate command:
```
/new-svelte-pwa
```
### Step 2: Provide Project Details
When prompted, provide:
- **Project name**: e.g., "expense-tracker"
- **Odoo model**: e.g., "expense" (without x_ prefix)
- **Display name**: e.g., "Expense"
- **Deployment target**: e.g., "vercel" (optional)
### Step 3: Initialize the Project
```
cd your-project-name
/init-project
```
### Step 4: Configure Odoo Connection
```
/setup-env
```
Provide your Odoo URL, database, API key, and username.
### Step 5: Test Everything
```
/test-connection
```
Verify your Odoo connection works correctly.
### Step 6: Start Developing
The dev server should be running. Open your browser and start customizing!
### Step 7: Deploy (when ready)
```
/deploy-vercel
```
Follow the prompts to deploy to production.
## Plugin Skills:
### create-odoo-pwa
Generates a complete PWA project from scratch.
**When to use**: Starting a new project
**What it generates**:
- Base configuration (package.json, vite.config, etc.)
- Odoo API client
- Cache stores with smart syncing
- Server-side API proxy
- UI components (forms, lists)
- PWA manifest and service worker
- Deployment configurations
- Complete documentation
### add-odoo-model
Adds integration for additional Odoo models.
**When to use**: Adding new data models to existing project
**What it generates**:
- Cache store for the new model
- API methods in Odoo client
- Form and list pages (optional)
- Navigation updates
### create-cache-store
Creates a standalone cache store.
**When to use**: Need custom caching logic
**What it generates**:
- Framework-specific cache store
- Smart caching logic
- Background sync
- Optimistic updates
## Best Practices:
### Security
- ✅ Keep API keys in `.env` file (never commit)
- ✅ Use different credentials for dev and production
- ✅ Set environment variables in hosting platform
- ✅ Rotate API keys periodically
### Development
- ✅ Test Odoo connection before coding
- ✅ Use version control (Git)
- ✅ Test offline functionality regularly
- ✅ Read generated CLAUDE.md for patterns
### Deployment
- ✅ Build and test locally first
- ✅ Use Vercel/Netlify for full functionality
- ✅ Set up continuous deployment from Git
- ✅ Monitor performance and errors
### Maintenance
- ✅ Keep dependencies updated
- ✅ Monitor Odoo API changes
- ✅ Test after Odoo upgrades
- ✅ Clear caches when schema changes
## Getting More Help:
### Example prompts to use this command:
- `/help` - Show this help document
- User: "How do I use the Odoo PWA plugin?"
- User: "What commands are available?"
### In Generated Projects:
- `README.md` - Getting started guide
- `CLAUDE.md` - Architecture patterns
- `API.md` - API client reference
### External Resources:
- Odoo API Documentation: https://www.odoo.com/documentation/
- SvelteKit: https://kit.svelte.dev/
- React: https://react.dev/
- Vue: https://vuejs.org/
- PWA Guide: https://web.dev/progressive-web-apps/
### Troubleshooting:
If you encounter issues:
1. Run `/test-connection` to diagnose
2. Check `/troubleshoot` for common solutions
3. Review browser console for errors
4. Verify Odoo configuration
5. Check generated documentation
### Support:
For bugs or feature requests:
- Check existing issues
- Review documentation thoroughly
- Provide detailed error messages
- Include environment details
## Framework-Specific Notes:
### SvelteKit (Recommended)
- Uses Svelte 5 runes syntax
- Server-side API routes work perfectly
- Best offline functionality
- Smallest bundle size
- Easiest to deploy
### React
- Modern React 18+ hooks
- Context API for state management
- Wide ecosystem support
- Popular and familiar
### Vue
- Vue 3 Composition API
- Pinia for state management
- Great developer experience
- Progressive framework
## Architecture Highlights:
### Data Flow
```
Component → Cache Store → API Client → Server Route → Odoo
```
### Caching Strategy
1. **Load from cache** (instant, may be stale)
2. **Check if stale** (> 5 minutes)
3. **Background sync** (fetch new data)
4. **Update cache** (localStorage + IndexedDB)
5. **Reactive update** (UI updates automatically)
### Sync Strategy
1. **Incremental fetch** - Only `id > lastRecordId`
2. **Partner resolution** - Batch fetch related records
3. **Optimistic updates** - UI updates before server
4. **Error recovery** - Graceful offline handling
## Tips and Tricks:
💡 **Use the right command for the job**
- Quick start? `/new-svelte-pwa`
- Need help? `/help` or `/troubleshoot`
- Deploy ready? `/deploy-vercel`
💡 **Test early and often**
- Run `/test-connection` after setup
- Test offline mode frequently
- Verify sync works correctly
💡 **Read the generated docs**
- CLAUDE.md has architecture details
- API.md has client method docs
- README.md has setup instructions
💡 **Customize to your needs**
- Start with generated code
- Modify UI components
- Add business logic
- Extend with new features
💡 **Deploy with confidence**
- Test build locally first
- Use environment variables properly
- Monitor after deployment
- Set up error tracking
## Summary:
The odoo-pwa-generator plugin makes it easy to create production-ready PWAs with Odoo integration. Use the commands above to generate, develop, deploy, and maintain your applications.
For more details on any command, just run that command and follow the prompts!
Happy coding! 🚀

202
commands/init-project.md Normal file
View File

@@ -0,0 +1,202 @@
Initialize a newly generated Odoo PWA project with all necessary setup steps.
## What this command does:
- Runs `npm install` to install all dependencies
- Creates `.env` file from `.env.example`
- Guides through environment configuration
- Tests the Odoo connection
- Starts the development server
- Opens the application in browser
- Provides next steps and documentation
## Prerequisites:
- Newly generated Odoo PWA project
- Node.js installed (v18 or higher)
- npm or pnpm package manager
- Internet connection for dependencies
## Steps:
### 1. Verify Project Structure
Check that required files exist:
-`package.json`
-`.env.example`
-`README.md`
-`src/lib/odoo.js` (or equivalent)
- ✅ Framework config file (svelte.config.js / vite.config.js)
### 2. Install Dependencies
```bash
npm install
```
Show progress and estimated time.
### 3. Environment Setup
Copy `.env.example` to `.env`:
```bash
cp .env.example .env
```
Ask the user to provide:
1. Odoo URL
2. Database name
3. API key
4. Username
5. Model configuration
Update `.env` file with provided values.
### 4. Verify Setup
Read `.env` and verify all required variables are set.
### 5. Test Odoo Connection
Run a quick connection test:
- Test authentication
- Verify model access
- Check permissions
If connection fails, offer to run full diagnostics (`/test-connection`).
### 6. Initialize Git (if not already)
```bash
git init
git add .
git commit -m "Initial commit: Odoo PWA generated"
```
### 7. Start Development Server
```bash
npm run dev
```
Framework-specific commands:
- **SvelteKit**: `npm run dev` (default port 5173)
- **React**: `npm run dev` (default port 5173)
- **Vue**: `npm run dev` (default port 5173)
### 8. Open in Browser
Automatically open browser to `http://localhost:5173`
## Example prompts to use this command:
- `/init-project` - Complete initialization wizard
- User: "Set up my new Odoo PWA"
- User: "Initialize the project"
- User: "Get my PWA running"
## Post-Initialization Checklist:
After successful initialization:
**Immediate Next Steps**:
1. Test the application in the browser
2. Verify data loads from Odoo
3. Test creating a new record
4. Test editing and deleting records
5. Verify offline functionality (disable network)
**Configuration**:
1. Review and customize PWA manifest (colors, icons, name)
2. Update application metadata
3. Configure deployment targets
4. Set up version control (Git)
**Development**:
1. Read the generated `CLAUDE.md` for architecture details
2. Review `API.md` for Odoo client documentation
3. Explore the codebase structure
4. Customize UI components and styling
**Testing**:
1. Test all CRUD operations
2. Verify sync functionality
3. Test offline mode
4. Check PWA installability
5. Test on mobile devices
**Deployment Preparation**:
1. Review deployment documentation
2. Set up hosting platform account (Vercel/Netlify/etc)
3. Prepare production Odoo API keys
4. Configure environment variables for production
## Helpful Resources:
Provide links to:
- Project `README.md`
- `CLAUDE.md` (architecture guide)
- `API.md` (API documentation)
- Odoo documentation
- Framework documentation (SvelteKit/React/Vue)
- PWA best practices
## Development Commands:
Remind the user of available commands:
```bash
npm run dev # Start development server
npm run build # Build for production
npm run preview # Preview production build
npm run lint # Run linter
npm run format # Format code
```
## Troubleshooting:
### npm install fails
- Check Node.js version (must be v18+)
- Clear npm cache: `npm cache clean --force`
- Delete `node_modules` and `package-lock.json`, try again
- Check internet connection
### .env configuration issues
- Verify all variables are set (no empty values)
- Check for typos in variable names
- Ensure no spaces around `=` signs
- Verify Odoo credentials are correct
### Development server won't start
- Check if port 5173 is already in use
- Try different port: `npm run dev -- --port 3000`
- Check for syntax errors in config files
- Review console error messages
### Odoo connection fails
- Run `/test-connection` for full diagnostics
- Verify `.env` file is loaded correctly
- Check Odoo server is accessible
- Verify API key is valid
### Build fails
- Check for TypeScript errors (if using TypeScript)
- Verify all dependencies are installed
- Check for missing environment variables
- Review build logs for specific errors
## After Initialization:
Display summary:
```
🎉 Odoo PWA Initialized Successfully!
📁 Project: [project-name]
🚀 Framework: [SvelteKit/React/Vue]
🔗 Dev Server: http://localhost:5173
📝 Model: [model-name]
✅ Next Steps:
1. Open http://localhost:5173 in your browser
2. Test data sync from Odoo
3. Read CLAUDE.md for architecture details
4. Customize the UI to match your needs
5. Run /deploy-vercel when ready to deploy
📚 Documentation:
- README.md - Getting started guide
- CLAUDE.md - Architecture and patterns
- API.md - Odoo API client reference
💡 Helpful Commands:
- /test-connection - Verify Odoo integration
- /add-model - Add more Odoo models
- /deploy-vercel - Deploy to production
- /odoo-help - Get plugin help
Happy coding! 🚀
```

33
commands/new-react-pwa.md Normal file
View File

@@ -0,0 +1,33 @@
Generate a new offline-first Progressive Web App using React framework with Odoo Studio backend integration.
## What this command does:
- Invokes the `create-odoo-pwa` skill with React as the framework
- Generates a complete PWA project with smart caching, offline support, and Odoo integration
- Creates all necessary configuration files, components, hooks, and deployment setups
## Required Information:
Before starting, gather:
1. **Project name** (kebab-case, e.g., "expense-tracker", "inventory-manager")
2. **Odoo model name** (without `x_` prefix, e.g., "expense", "inventory")
3. **Model display name** (human-readable, e.g., "Expense", "Inventory Item")
4. **Deployment target** (optional: vercel, github, cloudflare, netlify)
## Steps:
1. Ask the user for the required information listed above
2. Invoke the `create-odoo-pwa` skill with framework set to "react"
3. Generate the complete React PWA project structure
4. Create comprehensive documentation (README.md, CLAUDE.md, API.md)
5. Provide next steps for setup and deployment
## Example prompts to use this command:
- `/new-react-pwa` - Interactive mode, will ask for all parameters
- User: "Create a React PWA for tracking expenses with Odoo"
- User: "Generate an inventory management app using React and Odoo Studio"
## After generation:
Remind the user to:
1. Navigate to the project directory
2. Copy `.env.example` to `.env` and configure Odoo credentials
3. Run `npm install` to install dependencies
4. Run `npm run dev` to start development server
5. Test the Odoo connection and sync functionality

View File

@@ -0,0 +1,33 @@
Generate a new offline-first Progressive Web App using SvelteKit framework with Odoo Studio backend integration.
## What this command does:
- Invokes the `create-odoo-pwa` skill with SvelteKit as the framework
- Generates a complete PWA project with smart caching, offline support, and Odoo integration
- Creates all necessary configuration files, routes, components, and deployment setups
## Required Information:
Before starting, gather:
1. **Project name** (kebab-case, e.g., "expense-tracker", "inventory-manager")
2. **Odoo model name** (without `x_` prefix, e.g., "expense", "inventory")
3. **Model display name** (human-readable, e.g., "Expense", "Inventory Item")
4. **Deployment target** (optional: vercel, github, cloudflare, netlify)
## Steps:
1. Ask the user for the required information listed above
2. Invoke the `create-odoo-pwa` skill with framework set to "sveltekit"
3. Generate the complete SvelteKit PWA project structure
4. Create comprehensive documentation (README.md, CLAUDE.md, API.md)
5. Provide next steps for setup and deployment
## Example prompts to use this command:
- `/new-svelte-pwa` - Interactive mode, will ask for all parameters
- User: "Create a SvelteKit PWA for tracking expenses with Odoo"
- User: "Generate an inventory management app using SvelteKit and Odoo Studio"
## After generation:
Remind the user to:
1. Navigate to the project directory
2. Copy `.env.example` to `.env` and configure Odoo credentials
3. Run `npm install` to install dependencies
4. Run `npm run dev` to start development server
5. Test the Odoo connection and sync functionality

33
commands/new-vue-pwa.md Normal file
View File

@@ -0,0 +1,33 @@
Generate a new offline-first Progressive Web App using Vue framework with Odoo Studio backend integration.
## What this command does:
- Invokes the `create-odoo-pwa` skill with Vue as the framework
- Generates a complete PWA project with smart caching, offline support, and Odoo integration
- Creates all necessary configuration files, components, composables, and deployment setups
## Required Information:
Before starting, gather:
1. **Project name** (kebab-case, e.g., "expense-tracker", "inventory-manager")
2. **Odoo model name** (without `x_` prefix, e.g., "expense", "inventory")
3. **Model display name** (human-readable, e.g., "Expense", "Inventory Item")
4. **Deployment target** (optional: vercel, github, cloudflare, netlify)
## Steps:
1. Ask the user for the required information listed above
2. Invoke the `create-odoo-pwa` skill with framework set to "vue"
3. Generate the complete Vue PWA project structure
4. Create comprehensive documentation (README.md, CLAUDE.md, API.md)
5. Provide next steps for setup and deployment
## Example prompts to use this command:
- `/new-vue-pwa` - Interactive mode, will ask for all parameters
- User: "Create a Vue PWA for tracking expenses with Odoo"
- User: "Generate an inventory management app using Vue and Odoo Studio"
## After generation:
Remind the user to:
1. Navigate to the project directory
2. Copy `.env.example` to `.env` and configure Odoo credentials
3. Run `npm install` to install dependencies
4. Run `npm run dev` to start development server
5. Test the Odoo connection and sync functionality

642
commands/optimize.md Normal file
View File

@@ -0,0 +1,642 @@
Analyze and optimize your Odoo PWA for performance, bundle size, and user experience.
## What this command does:
- Analyzes bundle size
- Identifies performance bottlenecks
- Suggests optimizations
- Checks caching efficiency
- Reviews PWA configuration
- Provides actionable recommendations
---
## Quick Performance Check 🚀
### Run These Commands:
```bash
# Build for production
npm run build
# Analyze bundle size
npm run build -- --analyze
# Preview production build
npm run preview
# Run Lighthouse audit
# (Chrome DevTools → Lighthouse tab)
```
---
## Bundle Size Analysis 📦
### Check Current Size
```bash
# Build and see output
npm run build
# Typical output:
# dist/index.html 1.2 kB
# dist/assets/index-abc123.js 45.3 kB
# dist/assets/vendor-def456.js 120.5 kB
```
### Analyze Bundle Composition
```bash
# Install analyzer
npm install -D rollup-plugin-visualizer
# Add to vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';
export default {
plugins: [
// ...other plugins
visualizer({
open: true,
gzipSize: true,
brotliSize: true
})
]
};
# Build and open report
npm run build
# Opens stats.html in browser
```
### Target Bundle Sizes:
-**Excellent**: < 200 KB (gzipped)
- ⚠️ **Good**: 200-500 KB
-**Needs Work**: > 500 KB
---
## Optimization Strategies
### 1. Code Splitting 📂
#### Lazy Load Routes
**SvelteKit** (automatic):
```javascript
// Routes are auto-split
// src/routes/+page.svelte → separate chunk
```
**React**:
```javascript
import { lazy, Suspense } from 'react';
const ExpenseList = lazy(() => import('./ExpenseList'));
function App() {
return (
<Suspense fallback={<Loading />}>
<ExpenseList />
</Suspense>
);
}
```
**Vue**:
```javascript
const ExpenseList = () => import('./ExpenseList.vue');
const routes = [
{
path: '/expenses',
component: ExpenseList // Lazy loaded
}
];
```
#### Manual Code Splitting
```javascript
// Split heavy utility into separate chunk
const heavyUtil = await import('./heavyUtility.js');
heavyUtil.doSomething();
```
### 2. Tree Shaking 🌳
#### Remove Unused Code
```javascript
// Bad: Imports everything
import * as utils from './utils';
// Good: Import only what you need
import { formatDate, formatCurrency } from './utils';
```
#### Check for Unused Dependencies
```bash
npm install -g depcheck
depcheck
# Remove unused packages
npm uninstall <unused-package>
```
### 3. Optimize Dependencies 📚
#### Use Lighter Alternatives
```javascript
// Instead of moment.js (heavy)
import moment from 'moment'; // 72 KB
// Use date-fns (tree-shakeable)
import { format } from 'date-fns'; // ~2 KB
// Or native Date
new Date().toLocaleDateString();
```
#### Common Heavy Packages & Alternatives:
-**moment** (72 KB) → ✅ **date-fns** (tree-shakeable)
-**lodash** (whole) → ✅ **lodash-es** (individual imports)
-**axios** → ✅ **fetch** (built-in)
-**uuid** → ✅ **crypto.randomUUID()** (built-in)
### 4. Optimize Images 🖼️
#### Compress Images
```bash
# Install image optimizer
npm install -D vite-plugin-imagemin
# Add to vite.config.js
import viteImagemin from 'vite-plugin-imagemin';
export default {
plugins: [
viteImagemin({
gifsicle: { optimizationLevel: 7 },
optipng: { optimizationLevel: 7 },
mozjpeg: { quality: 80 },
svgo: { plugins: [{ removeViewBox: false }] }
})
]
};
```
#### Lazy Load Images
```javascript
// Native lazy loading
<img src="large-image.jpg" loading="lazy" />
// Or with IntersectionObserver
```
#### Use Appropriate Formats
- **WebP** for photos (smaller than JPEG)
- **SVG** for icons/logos
- **PNG** only when transparency needed
### 5. Minimize JavaScript ⚡
#### Enable Minification (default in Vite)
```javascript
// vite.config.js
export default {
build: {
minify: 'terser', // or 'esbuild' (faster)
terserOptions: {
compress: {
drop_console: true, // Remove console.logs
drop_debugger: true
}
}
}
};
```
#### Remove Development Code
```javascript
// Use environment variables
if (import.meta.env.DEV) {
console.log('Debug info'); // Removed in production
}
```
---
## Caching Optimization 💾
### 1. Optimize Cache Strategy
#### Review Cache Settings
```javascript
// In cache store
const CACHE_VALIDITY = 5 * 60 * 1000; // 5 minutes
// Consider your use case:
// - Frequently changing data: 1-2 minutes
// - Moderate updates: 5-10 minutes
// - Rarely changes: 30-60 minutes
```
#### Limit Initial Load
```javascript
// Don't fetch everything at once
const records = await odoo.searchRecords(
model,
[],
fields,
100 // Limit to 100 records initially
);
// Load more on demand (pagination)
```
### 2. Optimize Sync Frequency
```javascript
// Adjust sync interval based on needs
const SYNC_INTERVAL = 5 * 60 * 1000; // 5 minutes
// Balance:
// - More frequent: Better UX, more bandwidth
// - Less frequent: Less bandwidth, slightly stale data
```
### 3. Selective Caching
```javascript
// Only cache fields you need
const fields = [
'x_studio_name',
'x_studio_amount',
'x_studio_date'
// Don't fetch unnecessary fields
];
```
---
## PWA Optimization 📱
### 1. Service Worker Configuration
#### Optimize Caching Strategy
```javascript
// In service worker or vite-plugin-pwa config
VitePWA({
workbox: {
runtimeCaching: [
{
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts',
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 365 // 1 year
}
}
},
{
urlPattern: /^https:\/\/.*\.odoo\.com\/.*/,
handler: 'NetworkFirst',
options: {
cacheName: 'odoo-api',
networkTimeoutSeconds: 3
}
}
]
}
})
```
### 2. Optimize Manifest
```json
{
"name": "Expense Tracker",
"short_name": "Expenses",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#0066cc",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
```
### 3. Optimize Icons
```bash
# Use optimized PNG or WebP
# Compress images
# Provide multiple sizes
```
---
## Network Optimization 🌐
### 1. Reduce API Calls
#### Batch Requests
```javascript
// Bad: Multiple calls
const expenses = await fetchExpenses();
const tasks = await fetchTasks();
const partners = await fetchPartners();
// Good: Single call (if Odoo supports)
const data = await fetchAll(['expenses', 'tasks', 'partners']);
```
#### Cache Partner Names
```javascript
// Fetch once, reuse
const partners = await odoo.fetchPartners();
localStorage.setItem('partnerCache', JSON.stringify(partners));
// Later, use cached data
const cached = JSON.parse(localStorage.getItem('partnerCache'));
```
### 2. Incremental Loading
```javascript
// Load initial data
const initial = await fetchExpenses({ limit: 20 });
// Load more on scroll
function onScroll() {
if (nearBottom) {
loadMore();
}
}
```
### 3. Optimize Requests
```javascript
// Only fetch fields you display
const fields = ['id', 'x_studio_name', 'x_studio_amount'];
// Use appropriate domain filters
const domain = [
['x_studio_date', '>=', lastMonth],
['x_studio_status', '!=', 'deleted']
];
```
---
## UI Performance 🎨
### 1. Virtual Scrolling
For long lists:
```javascript
// SvelteKit
import { VirtualList } from 'svelte-virtual-list';
<VirtualList items={expenses} let:item>
<ExpenseCard expense={item} />
</VirtualList>
// React
import { FixedSizeList } from 'react-window';
// Vue
import { RecycleScroller } from 'vue-virtual-scroller';
```
### 2. Debounce Search
```javascript
import { debounce } from './utils';
const handleSearch = debounce((query) => {
searchExpenses(query);
}, 300); // Wait 300ms after typing stops
```
### 3. Optimize Rendering
```javascript
// SvelteKit: Use keyed each blocks
{#each expenses as expense (expense.id)}
<ExpenseCard {expense} />
{/each}
// React: Use keys and memo
const ExpenseCard = memo(({ expense }) => {
// ...
});
expenses.map(expense => (
<ExpenseCard key={expense.id} expense={expense} />
));
// Vue: Use v-memo
<ExpenseCard
v-for="expense in expenses"
:key="expense.id"
:expense="expense"
v-memo="[expense.id]"
/>
```
---
## Build Optimization 🔨
### Vite Config Optimizations
```javascript
// vite.config.js
export default {
build: {
// Target modern browsers
target: 'es2020',
// Optimize chunks
rollupOptions: {
output: {
manualChunks: {
vendor: ['svelte', '@sveltejs/kit'],
odoo: ['./src/lib/odoo.js', './src/lib/stores']
}
}
},
// Compression
minify: 'esbuild', // Faster than terser
// Source maps (only for debugging)
sourcemap: false
},
// Optimize dependencies
optimizeDeps: {
include: ['date-fns', 'lodash-es']
}
};
```
---
## Lighthouse Audit 💡
### Run Lighthouse
1. Open Chrome DevTools
2. Go to Lighthouse tab
3. Select categories:
- ✅ Performance
- ✅ Progressive Web App
- ✅ Best Practices
- ✅ Accessibility
- ✅ SEO
4. Click "Analyze page load"
### Target Scores:
- **Performance**: > 90
- **PWA**: 100
- **Best Practices**: > 95
- **Accessibility**: > 90
- **SEO**: > 90
### Common Issues & Fixes:
#### Low Performance Score
- Reduce bundle size
- Optimize images
- Enable caching
- Lazy load resources
#### PWA Issues
- Fix manifest.json
- Register service worker
- Add offline support
- Provide app icons
#### Accessibility Issues
- Add alt text to images
- Use semantic HTML
- Ensure color contrast
- Add ARIA labels
---
## Performance Monitoring 📊
### Add Performance Tracking
```javascript
// Measure initial load
window.addEventListener('load', () => {
const perfData = performance.getEntriesByType('navigation')[0];
console.log('Load time:', perfData.loadEventEnd - perfData.fetchStart);
console.log('DOM ready:', perfData.domContentLoadedEventEnd - perfData.fetchStart);
});
// Measure sync time
console.time('sync');
await expenseCache.refresh();
console.timeEnd('sync');
```
### Use Web Vitals
```bash
npm install web-vitals
# In app
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
getCLS(console.log);
getFID(console.log);
getFCP(console.log);
getLCP(console.log);
getTTFB(console.log);
```
---
## Optimization Checklist ✅
### Bundle Size:
```
□ Analyzed bundle composition
□ Removed unused dependencies
□ Lazy loaded routes/components
□ Optimized images
□ Tree-shaking enabled
□ Minification enabled
□ Total size < 500 KB (gzipped)
```
### Caching:
```
□ Optimal cache validity period
□ Incremental sync working
□ Selective field fetching
□ Partner name caching
□ Appropriate sync frequency
□ IndexedDB optimized
```
### PWA:
```
□ Service worker registered
□ Offline mode works
□ App installable
□ Icons optimized
□ Manifest configured
□ Fast load time
```
### Performance:
```
□ Lighthouse score > 90
□ Initial load < 3s
□ Sync time < 2s
□ No console errors
□ Smooth scrolling
□ Fast navigation
```
---
## Example prompts to use this command:
- `/optimize` - Run optimization checks
- User: "Make my app faster"
- User: "Reduce bundle size"
- User: "Optimize performance"
## Related Commands:
- `/test-connection` - Test after optimization
- `/update-deps` - Update to faster versions
- `/troubleshoot` - Fix performance issues
- `/help` - More information
---
## Quick Wins 🎯
### Immediate Optimizations:
1. **Enable compression** (already in Vite)
2. **Remove console.logs** in production
3. **Lazy load routes** (easy with frameworks)
4. **Optimize images** (use WebP)
5. **Limit initial data fetch** (pagination)
### Medium Effort:
1. **Analyze and split bundles**
2. **Replace heavy dependencies**
3. **Implement virtual scrolling**
4. **Optimize service worker**
5. **Add performance monitoring**
### Long Term:
1. **Regular Lighthouse audits**
2. **Monitor real user metrics**
3. **Continuous optimization**
4. **Keep dependencies updated**
5. **Review and refactor regularly**

87
commands/setup-env.md Normal file
View File

@@ -0,0 +1,87 @@
Interactive setup wizard for configuring Odoo PWA environment variables.
## What this command does:
- Guides the user through setting up their `.env` file
- Validates Odoo connection credentials
- Tests API connectivity
- Configures model-specific settings
- Provides troubleshooting help if connection fails
## Required Information:
Gather from the user:
1. **Odoo Instance URL** (e.g., "https://yourcompany.odoo.com")
2. **Database Name** (e.g., "yourcompany-main")
3. **API Key** (from Odoo user preferences)
4. **Username/Email** (Odoo user email)
5. **Primary Model Name** (e.g., "x_expense", "x_inventory")
## Steps:
1. Check if `.env.example` exists in the current directory
2. If not, ask if this is an Odoo PWA project
3. Ask the user for each environment variable interactively
4. Validate URL format (must start with http:// or https://)
5. Create or update `.env` file with the provided values
6. Test the connection by making a simple API call to Odoo
7. If connection fails, provide troubleshooting steps
8. Display success message with next steps
## Environment Variables to Set:
```bash
# Odoo Instance Configuration
VITE_ODOO_URL=https://yourcompany.odoo.com
VITE_ODOO_DB=yourcompany-main
# Authentication (keep these secret!)
ODOO_API_KEY=your_api_key_here
ODOO_USERNAME=your.email@company.com
# Model Configuration
VITE_MODEL_NAME=x_expense
VITE_MODEL_DISPLAY_NAME=Expense
```
## Validation Tests:
After creating `.env`, run these checks:
1. Test Odoo URL is reachable
2. Verify API key is valid
3. Check if the model exists in Odoo
4. Test read permissions on the model
5. Verify required fields are accessible
## Example prompts to use this command:
- `/setup-env` - Interactive setup wizard
- User: "Help me configure my Odoo credentials"
- User: "Set up environment variables for Odoo PWA"
## Security Reminders:
After setup, remind the user:
1.`.env` should be in `.gitignore` (verify this)
2. ✅ Never commit API keys to version control
3. ✅ Use different credentials for development and production
4. ✅ For deployment, set environment variables in the hosting platform
5. ✅ Rotate API keys periodically
## Troubleshooting Common Issues:
### Connection Failed
- Verify Odoo URL is correct and accessible
- Check if API key is valid (generate new one in Odoo)
- Ensure username matches the API key owner
- Check firewall/network restrictions
### Model Not Found
- Verify the model exists in Odoo Studio
- Check model name has `x_` prefix
- Ensure user has access permissions to the model
### Authentication Error
- Regenerate API key in Odoo (Settings → Users → API Keys)
- Verify database name is correct
- Check if account is active and not locked
## After Setup:
Remind the user to:
1. Restart the development server to load new environment variables
2. Test the application and verify data loads correctly
3. Keep the `.env.example` file updated for team members
4. Document any custom configuration in project README

230
commands/test-connection.md Normal file
View File

@@ -0,0 +1,230 @@
Test Odoo API connection and sync functionality to diagnose issues.
## What this command does:
- Validates Odoo connection credentials
- Tests API authentication
- Verifies model access and permissions
- Checks sync functionality
- Runs diagnostic tests on cache stores
- Provides detailed error reporting and solutions
## Prerequisites:
Before testing, ensure:
1.`.env` file exists and is configured
2. ✅ Current directory is an Odoo PWA project
3. ✅ Development server can be started
4. ✅ Internet connection is available
## Diagnostic Tests to Run:
### 1. Environment Configuration Check
Verify all required environment variables are set:
- `VITE_ODOO_URL` - Odoo instance URL
- `VITE_ODOO_DB` - Database name
- `ODOO_API_KEY` - API authentication key
- `ODOO_USERNAME` - User email/username
- `VITE_MODEL_NAME` - Primary model name
### 2. Network Connectivity Test
```bash
# Test if Odoo URL is reachable
curl -I [ODOO_URL]
```
Expected: HTTP 200 or 301/302 redirect
### 3. API Authentication Test
Make a test API call to verify credentials:
```javascript
POST /api/odoo
{
"action": "search",
"model": "res.partner",
"domain": [],
"fields": ["id", "name"],
"limit": 1
}
```
Expected: Returns at least one partner record
### 4. Model Access Test
Verify the configured model exists and is accessible:
```javascript
POST /api/odoo
{
"action": "search_model",
"model": "[VITE_MODEL_NAME]",
"domain": [],
"fields": ["id"],
"limit": 1
}
```
Expected: Returns records or empty array (not an error)
### 5. CRUD Operations Test
Test each operation:
- **Create**: Create a test record
- **Read**: Fetch the created record
- **Update**: Modify the record
- **Delete**: Remove the test record
### 6. Cache Functionality Test
Verify cache stores work correctly:
- localStorage read/write
- IndexedDB read/write
- Cache expiration logic
- Sync mechanism
### 7. Sync Performance Test
Measure sync performance:
- Initial load time
- Incremental sync time
- Number of records synced
- Network request count
## Steps:
1. Read and validate `.env` file
2. Parse environment variables
3. Run each diagnostic test in sequence
4. Log results with timestamps
5. Identify failing tests
6. Provide specific solutions for failures
7. Generate diagnostic report
## Output Format:
```
🧪 Odoo PWA Connection Diagnostics
================================
✅ Environment Configuration: PASSED
- ODOO_URL: https://yourcompany.odoo.com
- DATABASE: yourcompany-main
- MODEL: x_expense
✅ Network Connectivity: PASSED
- Odoo server reachable
- Response time: 234ms
✅ API Authentication: PASSED
- API key valid
- User authenticated: your.email@company.com
✅ Model Access: PASSED
- Model exists: x_expense
- Read permission: Yes
- Write permission: Yes
✅ CRUD Operations: PASSED
- Create: ✅ Record created (ID: 123)
- Read: ✅ Record fetched
- Update: ✅ Record updated
- Delete: ✅ Record deleted
✅ Cache Functionality: PASSED
- localStorage: Working
- IndexedDB: Working
- Sync interval: 3 minutes
✅ Sync Performance: PASSED
- Initial load: 1.2s (45 records)
- Incremental sync: 0.3s (2 new records)
- Network requests: 3
================================
🎉 All tests passed! Your Odoo PWA is working correctly.
```
## Example prompts to use this command:
- `/test-connection` - Run full diagnostic suite
- User: "Test my Odoo connection"
- User: "Why isn't data syncing from Odoo?"
- User: "Diagnose Odoo API issues"
## Common Issues and Solutions:
### ❌ Connection Failed
**Error**: Cannot reach Odoo URL
**Solutions**:
- Verify URL is correct (include https://)
- Check internet connection
- Test URL in browser
- Check firewall/VPN settings
### ❌ Authentication Failed
**Error**: Invalid API key or credentials
**Solutions**:
- Regenerate API key in Odoo (Settings → Users → API Keys)
- Verify username matches API key owner
- Check for typos in `.env` file
- Ensure API key has not expired
### ❌ Model Not Found
**Error**: Model doesn't exist or no access
**Solutions**:
- Verify model exists in Odoo Studio
- Check model name includes `x_` prefix
- Verify user has read/write permissions
- Check model is published (not in draft mode)
### ❌ CORS Error
**Error**: Blocked by CORS policy
**Solutions**:
- Use server-side API proxy (recommended)
- Configure CORS in Odoo (not recommended)
- Check API route is working correctly
### ❌ Sync Not Working
**Error**: Records not updating
**Solutions**:
- Check browser console for errors
- Verify sync interval is running
- Clear cache and try again
- Check Odoo server is not down
### ❌ Permission Denied
**Error**: Cannot create/update records
**Solutions**:
- Verify user has write permission on model
- Check required fields are included
- Verify field types match expectations
- Check for validation rules in Odoo
## Development Tools:
Provide user with helpful commands:
```bash
# Watch network requests
# Open browser DevTools → Network tab
# View cache contents
localStorage.getItem('[model]Cache')
# Force cache clear
localStorage.clear()
# Monitor sync in console
# Look for "Syncing..." messages
```
## After Testing:
If all tests pass:
- Confirm the application is working correctly
- Suggest running tests periodically
- Recommend monitoring in production
If tests fail:
- Provide specific error messages
- Offer step-by-step troubleshooting
- Suggest checking Odoo server logs
- Offer to help fix configuration
## Advanced Diagnostics:
For complex issues:
1. Export full diagnostic report
2. Check Odoo server logs
3. Review browser console errors
4. Analyze network traffic
5. Test with different API keys
6. Try with different models
7. Compare with working examples

694
commands/troubleshoot.md Normal file
View File

@@ -0,0 +1,694 @@
Common issues and solutions for Odoo PWA projects.
## What this command does:
- Lists common problems and their solutions
- Provides step-by-step debugging guides
- Offers troubleshooting workflows
- Helps diagnose connection, sync, and build issues
- Links to relevant documentation and tools
---
## Quick Diagnostic Checklist ✅
Before diving into specific issues, run through this checklist:
```
□ .env file exists and has all required variables
□ Odoo URL is correct and reachable
□ API key is valid and not expired
□ Database name is correct
□ Model name includes x_ prefix
□ Development server is running
□ No errors in browser console
□ Internet connection is working
□ Node.js version is 18 or higher
```
**Quick Test:** Run `/test-connection` to diagnose most issues automatically.
---
## Connection Issues 🔌
### Problem: Cannot connect to Odoo
**Symptoms:**
- "Connection refused" error
- "Network error" in console
- Timeout errors
- No data loading
**Solutions:**
#### 1. Verify Odoo URL
```bash
# Test if URL is reachable
curl -I https://yourcompany.odoo.com
# Expected: HTTP 200 or 301/302
```
**Common mistakes:**
- Missing `https://`
- Extra trailing slash
- Wrong subdomain
- Typo in URL
**Fix:**
```bash
# Wrong
VITE_ODOO_URL=yourcompany.odoo.com
VITE_ODOO_URL=https://yourcompany.odoo.com/
# Right
VITE_ODOO_URL=https://yourcompany.odoo.com
```
#### 2. Check firewall / VPN
- Try accessing Odoo URL in browser
- Disable VPN temporarily
- Check corporate firewall rules
- Try from different network
#### 3. Verify environment variables loaded
```javascript
// Add to your code temporarily
console.log('Odoo URL:', import.meta.env.VITE_ODOO_URL);
console.log('Database:', import.meta.env.VITE_ODOO_DB);
```
**If undefined:**
- Restart development server
- Check variable names (case-sensitive)
- Ensure variables start with `VITE_`
---
## Authentication Issues 🔐
### Problem: Invalid API key / Authentication failed
**Symptoms:**
- "Invalid credentials" error
- "Access denied" message
- 401 Unauthorized errors
- UID is null
**Solutions:**
#### 1. Regenerate API Key
1. Log into Odoo
2. Go to Settings → Users & Companies → Users
3. Open your user record
4. Go to "API Keys" tab
5. Click "New API Key"
6. Copy the key immediately (shown only once!)
7. Update `.env` file:
```bash
ODOO_API_KEY=your_new_api_key_here
```
8. Restart development server
#### 2. Verify username matches
```bash
# Username must match the API key owner
ODOO_USERNAME=john.doe@company.com # ✅ Correct
ODOO_USERNAME=John Doe # ❌ Wrong
```
#### 3. Check user permissions
- Ensure user has access to the model
- Verify read/write permissions
- Check if user account is active
- Verify not locked out
#### 4. Test authentication manually
```javascript
// In browser console
fetch('/api/odoo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'search',
model: 'res.partner',
domain: [],
fields: ['name'],
limit: 1
})
})
.then(r => r.json())
.then(console.log);
```
---
## Model Access Issues 📋
### Problem: Model not found / No records returned
**Symptoms:**
- "Model does not exist" error
- Empty array returned
- "Access rights" error
- Records don't appear
**Solutions:**
#### 1. Verify model name
```bash
# Check model name includes x_ prefix
VITE_MODEL_NAME=x_expense # ✅ Correct
VITE_MODEL_NAME=expense # ❌ Wrong
```
#### 2. Check model exists in Odoo
1. Log into Odoo
2. Go to Settings → Technical → Database Structure → Models
3. Search for your model name
4. Verify it exists and is active
#### 3. Check field names
```javascript
// Fields must use full name with x_studio_ prefix
const fields = [
'x_studio_name', // ✅ Correct
'x_studio_amount', // ✅ Correct
'name', // ❌ Wrong (unless it's a standard field)
'amount' // ❌ Wrong
];
```
#### 4. Verify permissions
- User must have read access to model
- Check access rights in Odoo Studio
- Verify record rules don't filter all records
- Test with admin user
#### 5. Check if model is published
- In Odoo Studio, verify model is not in draft mode
- Ensure model is accessible via API
- Check if model requires special access
---
## Sync Issues 🔄
### Problem: Data not syncing
**Symptoms:**
- Old data displayed
- Changes don't appear
- "Last synced" time not updating
- Background sync not working
**Solutions:**
#### 1. Check browser console
Look for:
- Network errors
- JavaScript errors
- CORS errors
- Authentication errors
#### 2. Verify sync mechanism
```javascript
// Check if sync is running
// Look for these console logs:
"Syncing x_expense..."
"Fetched X new records"
"Cache updated"
```
#### 3. Force refresh
```javascript
// In browser console
expenseCache.refresh();
```
#### 4. Clear cache and reload
```javascript
// In browser console
localStorage.clear();
// Then refresh page
```
#### 5. Check sync interval
```javascript
// Verify sync timer is running
// Default: 3 minutes (180,000ms)
// Look in cache store for:
setInterval(() => sync(), 180000);
```
#### 6. Test incremental sync
```javascript
// Check lastRecordId is updating
const cacheData = localStorage.getItem('expenseCache');
console.log(JSON.parse(cacheData));
// Should show: { lastRecordId: X, lastSyncTime: Y }
```
---
## Offline Mode Issues 📵
### Problem: Offline mode not working
**Symptoms:**
- App doesn't work without internet
- "No connection" errors when offline
- Service worker not registered
- Data not available offline
**Solutions:**
#### 1. Verify service worker registered
```javascript
// In browser console
navigator.serviceWorker.getRegistration()
.then(reg => console.log('Service Worker:', reg));
```
#### 2. Check cache stores
```javascript
// Verify data is cached
localStorage.getItem('expenseCache');
// Should return JSON string
// Check IndexedDB
// Open DevTools → Application → IndexedDB
// Look for your database and tables
```
#### 3. Test offline mode
1. Load app while online
2. Open DevTools → Network tab
3. Enable "Offline" checkbox
4. Refresh page
5. App should still work
#### 4. Build and test production
```bash
# Offline mode works better in production build
npm run build
npm run preview
```
#### 5. Check manifest.json
Verify PWA manifest is correct:
- Located in `/static/manifest.json`
- Has correct `start_url`
- Has valid icons
---
## Build / Deployment Issues 🚀
### Problem: Build fails
**Symptoms:**
- `npm run build` returns errors
- TypeScript errors
- Module not found errors
- Build process hangs
**Solutions:**
#### 1. Check Node version
```bash
node --version
# Must be v18 or higher
# If too old:
nvm install 18
nvm use 18
```
#### 2. Clean install
```bash
rm -rf node_modules package-lock.json
npm install
```
#### 3. Check for errors
```bash
# Run build with verbose output
npm run build -- --verbose
```
#### 4. Verify environment variables
```bash
# For production build, set env vars:
VITE_ODOO_URL=https://yourcompany.odoo.com \
VITE_ODOO_DB=yourcompany-main \
npm run build
```
#### 5. Check imports
- Verify all imports are correct
- Check for circular dependencies
- Ensure all files exist
---
### Problem: Deployment fails
**Symptoms:**
- Vercel/Netlify build fails
- Environment variables not working
- 404 errors in production
- Blank page after deployment
**Solutions:**
#### 1. Set environment variables
In hosting dashboard:
- Add all `VITE_*` variables
- Add `ODOO_API_KEY`
- Add `ODOO_USERNAME`
- Redeploy after adding
#### 2. Check build logs
- Review deployment logs
- Look for specific errors
- Check Node version in platform
#### 3. Test build locally
```bash
npm run build
npm run preview
# Test at http://localhost:4173
```
#### 4. Verify base path
```javascript
// For GitHub Pages or subpath deployment
// vite.config.js
export default {
base: '/your-repo-name/'
};
```
#### 5. Check API routes
- Ensure serverless functions deploy correctly
- Verify function size < 50MB
- Check function logs in platform dashboard
---
## Performance Issues ⚡
### Problem: App is slow
**Symptoms:**
- Slow initial load
- Laggy UI
- Long sync times
- High memory usage
**Solutions:**
#### 1. Optimize initial load
```javascript
// Limit initial fetch
const records = await odoo.searchRecords(
model,
[],
fields,
100 // Only fetch first 100
);
```
#### 2. Add pagination
```javascript
// Load more on scroll
let page = 1;
const pageSize = 20;
async function loadMore() {
const offset = (page - 1) * pageSize;
const more = await odoo.searchRecords(
model, [], fields, pageSize, offset
);
page++;
return more;
}
```
#### 3. Optimize bundle size
```bash
# Analyze bundle
npm run build -- --analyze
# Lazy load routes
// SvelteKit (automatic)
// React
const ExpenseList = lazy(() => import('./ExpenseList'));
// Vue
const ExpenseList = () => import('./ExpenseList.vue');
```
#### 4. Reduce sync frequency
```javascript
// Increase sync interval
const SYNC_INTERVAL = 5 * 60 * 1000; // 5 minutes instead of 3
```
#### 5. Optimize images
- Compress images before upload
- Use appropriate image formats
- Lazy load images
---
## Data Issues 📊
### Problem: Wrong data displayed
**Symptoms:**
- Incorrect values shown
- Missing fields
- Corrupted data
- Date format issues
**Solutions:**
#### 1. Clear cache
```javascript
expenseCache.clearCache();
expenseCache.refresh();
```
#### 2. Verify field mapping
```javascript
// Check if fields are correctly named
console.log(records[0]);
// Should show x_studio_* fields
```
#### 3. Check data types
```javascript
// Ensure correct types
const expense = {
x_studio_amount: 45.50, // number ✅
x_studio_date: '2025-01-15', // string (ISO) ✅
x_studio_employee: [12, false] // Many2one ✅
};
```
#### 4. Format dates correctly
```javascript
// Store as ISO string
const dateString = '2025-01-15';
// Display formatted
new Date(dateString).toLocaleDateString();
```
#### 5. Handle Many2one fields
```javascript
// Many2one can be [id, name] or just id
function getPartnerId(field) {
return Array.isArray(field) ? field[0] : field;
}
function getPartnerName(field) {
return Array.isArray(field) ? field[1] : null;
}
```
---
## CORS Issues 🚫
### Problem: CORS error
**Symptoms:**
- "Blocked by CORS policy" in console
- Requests fail in browser but work in Postman
- Preflight errors (OPTIONS)
**Solutions:**
#### 1. Use server-side proxy (recommended)
All generated PWAs include server-side API routes. Ensure you're using them:
```javascript
// ✅ Good: Goes through server proxy
fetch('/api/odoo', { ... });
// ❌ Bad: Direct call to Odoo (CORS error)
fetch('https://yourcompany.odoo.com/jsonrpc', { ... });
```
#### 2. Verify API route works
```javascript
// Test in browser console
fetch('/api/odoo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'search',
model: 'res.partner',
domain: [],
fields: ['name'],
limit: 1
})
})
.then(r => r.json())
.then(console.log);
```
#### 3. Check if deployed correctly
- Verify serverless functions deployed
- Check function logs
- Ensure environment variables set
---
## Browser-Specific Issues 🌐
### Problem: Works in Chrome but not Safari/Firefox
**Symptoms:**
- Different behavior across browsers
- Safari-specific errors
- Mobile browser issues
**Solutions:**
#### 1. Check browser compatibility
- Test in multiple browsers
- Check for console errors in each
- Verify IndexedDB support
#### 2. Safari-specific
- Safari has stricter storage limits
- Check if localStorage/IndexedDB working
- Test in private mode
#### 3. Mobile browsers
- Test on actual devices
- Check responsive design
- Verify touch interactions work
---
## Debugging Tools & Commands 🔧
### Essential Commands
```bash
# Test connection
/test-connection
# View full help
/help
# See architecture details
/architecture
# Clear cache and start fresh
/clear-cache
# Fix sync issues
/fix-sync
```
### Browser DevTools
```javascript
// Network tab
// - See all API calls
// - Check request/response
// - View timing
// Console tab
// - See error messages
// - Run test commands
// - Inspect data
// Application tab
// - View localStorage
// - Inspect IndexedDB
// - Check Service Workers
// - View Cache Storage
// Performance tab
// - Profile slow operations
// - Find bottlenecks
```
### Useful Console Commands
```javascript
// View cache
localStorage.getItem('expenseCache');
// Clear cache
localStorage.clear();
// Force refresh
expenseCache.refresh();
// View current records
console.log($expenseCache); // SvelteKit
// Test API
fetch('/api/odoo', { /* ... */ })
.then(r => r.json())
.then(console.log);
```
---
## Getting Help 💬
### Before asking for help:
1. Run `/test-connection`
2. Check browser console for errors
3. Review this troubleshooting guide
4. Try solutions listed above
5. Test with minimal example
### When reporting issues:
Include:
- Exact error message
- Browser and version
- Node.js version
- Steps to reproduce
- What you've already tried
- Relevant code snippets
### Helpful resources:
- `/help` - Plugin documentation
- `/examples` - Usage examples
- `/architecture` - Design patterns
- `/api-reference` - API documentation
- Odoo docs: https://www.odoo.com/documentation/
- Framework docs (SvelteKit/React/Vue)
---
## Example prompts to use this command:
- `/troubleshoot` - Show troubleshooting guide
- User: "Why isn't my data syncing?"
- User: "I'm getting a connection error"
- User: "Help! Nothing works!"
## Still Stuck?
If these solutions don't help:
1. Run `/test-connection` for detailed diagnostics
2. Check generated project's CLAUDE.md
3. Review Odoo server logs
4. Test with different Odoo model
5. Try generating fresh project to compare
Most issues are related to configuration or permissions. Double-check your `.env` file and Odoo settings!

505
commands/update-deps.md Normal file
View File

@@ -0,0 +1,505 @@
Update dependencies in your Odoo PWA project to latest compatible versions.
## What this command does:
- Checks for outdated dependencies
- Updates packages to latest versions
- Tests the application after updates
- Fixes breaking changes if needed
- Updates lock files
- Verifies everything still works
## Prerequisites:
- Current directory is an Odoo PWA project
- Git repository (recommended for easy rollback)
- Latest npm installed
---
## Update Strategy
### Safe Update (Recommended)
Updates to latest compatible versions within semver ranges.
```bash
npm update
```
### Major Update (Requires testing)
Updates to latest versions including major releases.
```bash
npm outdated # See what's outdated
npm update # Update minor/patch
npx npm-check-updates -u # Update majors
npm install
```
---
## Steps:
### 1. Check Current Status
```bash
# See current versions
npm list --depth=0
# See outdated packages
npm outdated
```
### 2. Create Backup
```bash
# Commit current state
git add .
git commit -m "Pre-dependency update checkpoint"
# Or backup package files
cp package.json package.json.backup
cp package-lock.json package-lock.json.backup
```
### 3. Update Dependencies
#### Minor/Patch Updates (Safe)
```bash
npm update
npm install
```
#### Major Updates (Test carefully)
```bash
# Install npm-check-updates if needed
npm install -g npm-check-updates
# Preview updates
ncu
# Update package.json
ncu -u
# Install new versions
npm install
```
### 4. Framework-Specific Updates
#### SvelteKit
```bash
# Update Svelte + SvelteKit
npm update @sveltejs/kit @sveltejs/adapter-static
npm update svelte
# Check for breaking changes
# https://github.com/sveltejs/kit/blob/master/CHANGELOG.md
```
#### React
```bash
# Update React
npm update react react-dom
# Update related packages
npm update @types/react @types/react-dom
```
#### Vue
```bash
# Update Vue
npm update vue
# Update related packages
npm update @vitejs/plugin-vue
```
### 5. Update Build Tools
```bash
# Update Vite
npm update vite
# Update PWA plugin
npm update vite-plugin-pwa @vite-pwa/sveltekit
```
### 6. Test Everything
#### Run Development Server
```bash
npm run dev
```
Check:
- ✅ Server starts without errors
- ✅ App loads correctly
- ✅ No console errors
- ✅ All routes work
#### Test Core Functionality
- ✅ Odoo connection works
- ✅ Data loads from cache
- ✅ Sync works
- ✅ CRUD operations work
- ✅ Offline mode works
#### Build for Production
```bash
npm run build
```
Check:
- ✅ Build completes without errors
- ✅ No warnings about deprecations
- ✅ Bundle size is reasonable
#### Preview Production Build
```bash
npm run preview
```
Test the same functionality in production mode.
### 7. Run Connection Test
```bash
# If /test-connection command is available
/test-connection
```
### 8. Commit Changes
```bash
git add package.json package-lock.json
git commit -m "Update dependencies to latest versions"
```
---
## Common Issues & Solutions
### Issue: Build fails after update
**Solution:**
```bash
# Revert updates
git checkout package.json package-lock.json
npm install
# Try updating one package at a time
npm update vite
npm run build # Test
npm update @sveltejs/kit
npm run build # Test
# Continue one by one
```
### Issue: TypeScript errors
**Solution:**
```bash
# Update TypeScript
npm update typescript
# Update type definitions
npm update @types/node
# Regenerate tsconfig
npx tsc --init
```
### Issue: Import errors
**Solution:**
```javascript
// Old import might be:
import { foo } from 'package';
// New import might be:
import { foo } from 'package/foo';
// Check package's CHANGELOG for migration guide
```
### Issue: Service Worker errors
**Solution:**
```bash
# Update PWA plugin
npm update vite-plugin-pwa
# Check configuration
# vite.config.js
VitePWA({
// Update config as needed
})
```
### Issue: Peer dependency warnings
**Solution:**
```bash
# Install peer dependencies
npm install <missing-peer-dep>
# Or use --force (not recommended)
npm install --force
```
---
## Breaking Changes to Watch For
### Vite 5+
- May require Node 18+
- ESM-only packages
- Updated config format
### SvelteKit 2+
- Svelte 5 syntax (runes)
- Updated adapter config
- New routing conventions
### React 19+
- New JSX transform
- Updated hooks behavior
- Server components
### Vue 3.4+
- New compiler optimizations
- Updated Composition API
- Better TypeScript support
---
## Update Checklist
After updating, verify:
```
□ npm run dev works
□ npm run build succeeds
□ npm run preview works
□ No console errors
□ Odoo connection works
□ Data syncs correctly
□ CRUD operations work
□ Offline mode works
□ Service worker registered
□ Tests pass (if any)
□ No TypeScript errors
□ No ESLint warnings
□ Documentation updated
```
---
## Selective Updates
### Update Only Production Dependencies
```bash
npm update --prod
```
### Update Specific Package
```bash
npm update vite
npm update @sveltejs/kit
```
### Update to Specific Version
```bash
npm install vite@5.0.0
npm install svelte@5.0.0
```
### Keep Package at Current Version
```json
// package.json
{
"dependencies": {
"some-package": "1.2.3" // No ^ or ~ = exact version
}
}
```
---
## Best Practices
### 1. Update Regularly
- Check weekly for security updates
- Update monthly for feature updates
- Test before deploying
### 2. Read Changelogs
- Check CHANGELOG.md for breaking changes
- Review migration guides
- Test thoroughly
### 3. Update One Category at a Time
```bash
# 1. Framework
npm update svelte @sveltejs/kit
# 2. Build tools
npm update vite
# 3. Dependencies
npm update # All others
```
### 4. Keep Lock Files
- Commit `package-lock.json`
- Ensure consistent installs
- Track exact versions
### 5. Test in Staging
- Deploy to staging first
- Test all functionality
- Then deploy to production
---
## Security Updates
### Check for Vulnerabilities
```bash
npm audit
```
### Fix Automatically
```bash
npm audit fix
```
### Fix with Breaking Changes
```bash
npm audit fix --force
```
### Review Details
```bash
npm audit --json
```
---
## Automated Updates (Optional)
### Using Dependabot (GitHub)
Create `.github/dependabot.yml`:
```yaml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
```
### Using Renovate
Create `renovate.json`:
```json
{
"extends": ["config:base"],
"schedule": ["before 10am on monday"]
}
```
---
## Rollback Plan
### If Something Breaks
#### Quick Rollback
```bash
git checkout package.json package-lock.json
npm install
```
#### Restore from Backup
```bash
cp package.json.backup package.json
cp package-lock.json.backup package-lock.json
npm install
```
#### Git Reset
```bash
git reset --hard HEAD~1
npm install
```
---
## After Update
### Update Documentation
- Note any breaking changes
- Update README if needed
- Document new features used
### Notify Team
- Share what was updated
- Note any changes in behavior
- Update staging/production
### Monitor Production
- Watch error logs
- Check performance metrics
- Monitor user reports
---
## Example prompts to use this command:
- `/update-deps` - Update all dependencies
- User: "Update my dependencies"
- User: "Check for package updates"
- User: "My packages are outdated"
## Related Commands:
- `/fix-sync` - If sync breaks after update
- `/test-connection` - Verify Odoo still works
- `/troubleshoot` - Fix issues after update
- `/optimize` - Check for optimizations after update
---
## Quick Update Script
Save as `update.sh`:
```bash
#!/bin/bash
echo "Backing up..."
cp package.json package.json.backup
cp package-lock.json package-lock.json.backup
echo "Checking for updates..."
npm outdated
echo "Updating..."
npm update
echo "Installing..."
npm install
echo "Testing..."
npm run build
if [ $? -eq 0 ]; then
echo "✅ Build successful!"
echo "Run: npm run dev to test"
else
echo "❌ Build failed! Rolling back..."
cp package.json.backup package.json
cp package-lock.json.backup package-lock.json
npm install
fi
```
Usage:
```bash
chmod +x update.sh
./update.sh
```

317
plugin.lock.json Normal file
View File

@@ -0,0 +1,317 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:jamshu/jamshi-marketplace:plugins/odoo-pwa-generator",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "5a22a60b81300ee2a78ccc875705853c095f4309",
"treeHash": "9b11048764766904a04057b43ba25a1e814d8ba8cc0ba6a060d2a5fa3f091d7b",
"generatedAt": "2025-11-28T10:17:57.515365Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "odoo-pwa-generator",
"description": "Generate offline-first Progressive Web Apps with Odoo Studio backend integration. Supports SvelteKit, React, and Vue with smart caching, IndexedDB storage, and automatic sync.",
"version": "1.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "ef03b999ee46b25f1d3b2d72239e7d71f2b58ed77fc9cd3588ac501f257ee670"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "4895928ae8a33f11eebdcca2a0a282d66d40ae9af5740bd39d75a9719726e672"
},
{
"path": "commands/examples.md",
"sha256": "e140de867507ad7c2c67ebc4f2983419f6bc678b9eaf656a65bee3fd37170966"
},
{
"path": "commands/update-deps.md",
"sha256": "db7ce2f1d33b4e3be180d2a4129b2a48944034aec6c1295105949e25153c4119"
},
{
"path": "commands/architecture.md",
"sha256": "1126a8d2fc73b2697782ea656af21d6e8fe714ed0751fde16d1d6ac4bdf8abd9"
},
{
"path": "commands/troubleshoot.md",
"sha256": "8461b27091eca54d44425f64a64036718098b0020604867850dde13424362811"
},
{
"path": "commands/deploy-github.md",
"sha256": "4d1c015430ef38ae6512fdd726d9bae2e6f65f4c678b75c05274d2b59a2d4224"
},
{
"path": "commands/setup-env.md",
"sha256": "e46e36fc6897149548a3f1803a6a2bf875469eca8d5874c4097c8ebb692cb097"
},
{
"path": "commands/clear-cache.md",
"sha256": "57b445c3689e9aa34cbc3de708ee373c2f22575e42214566ef2e67215d505b9d"
},
{
"path": "commands/add-deployment.md",
"sha256": "65a4344771062f817fac5afc78377f9aaa2d74c25846334dba02dca9964ca6b3"
},
{
"path": "commands/help.md",
"sha256": "401ec2e63f86494dbeed65c79aac1ca9ba7e088203e9dc8ffd3954b86709a05b"
},
{
"path": "commands/new-svelte-pwa.md",
"sha256": "6a3eb3535714f34c60c5ffc585fab1c96cd4e6f1849b8deee6c459e0788ec361"
},
{
"path": "commands/fix-sync.md",
"sha256": "e6ebc3c57ffc6186640270a6ddd57bf91060b6270a85b900956846c086d9ad13"
},
{
"path": "commands/init-project.md",
"sha256": "35585e5b456a9f9bb58e07bc56121942649d0d0db9092fb21a1eadcaf3884c33"
},
{
"path": "commands/optimize.md",
"sha256": "0a8f5de1577f6f5e4d844fe247656ff62c559ca40d5b8a6badeafc7348600c65"
},
{
"path": "commands/test-connection.md",
"sha256": "001116c2c4c6d05c9feb4c4976004ad5949b92dc409275a45546b788e7791ab7"
},
{
"path": "commands/deploy-vercel.md",
"sha256": "0d82d6cf2553b611c8e00ab327368051cc6cf9a1a402bd03b33fe7872f5ef35d"
},
{
"path": "commands/new-react-pwa.md",
"sha256": "b762aec24b9fdcb1a01eecb257f420cb75f6988f5414bd520ebd1758ffbb0278"
},
{
"path": "commands/create-cache-store.md",
"sha256": "8eb3a44c1f8f143542541c48c38b3e4205bebc4d4f1d2ebcaa1c7a05f43bab79"
},
{
"path": "commands/new-vue-pwa.md",
"sha256": "80134258e0aa6978bcd1723ef56e3303451953d6ecc0e41b604cee310f695134"
},
{
"path": "commands/api-reference.md",
"sha256": "c0cca235884ed3064c68d597a058115b9fd8cc2c977f7fc81a8c429cc1e1d6dd"
},
{
"path": "commands/add-model.md",
"sha256": "86b0bc93c3dfb08a8369adc9f64c551bd0975fd32ceedb7ba3fada71e9faef60"
},
{
"path": "skills/add-odoo-model/SKILL.md",
"sha256": "99b766d9b01e5c92293d9c0b585c748060192a97331f1a33066fc6f0abba09ba"
},
{
"path": "skills/create-odoo-pwa/SKILL.md",
"sha256": "9eeee7636a24f9051b1441b3f4f14ee0c4ab6b89eb571100912ab6a18c652575"
},
{
"path": "skills/create-odoo-pwa/templates/vue/base/index.html.template",
"sha256": "7ee408b732e99165bd567a9f363b586be6b2a4cd6af56db40f7db90345861a80"
},
{
"path": "skills/create-odoo-pwa/templates/vue/base/.gitignore.template",
"sha256": "e6e6490c928492f52d02a247361a98b061d7c6bb929bfde885cd9de760826b13"
},
{
"path": "skills/create-odoo-pwa/templates/vue/base/.env.example.template",
"sha256": "a37594d8960fb1274c8163c25b285e7e6530a43f65ff8035805196167eec8be2"
},
{
"path": "skills/create-odoo-pwa/templates/vue/base/package.json.template",
"sha256": "2c9d9a42b41d58bb03d3465879462f76e088a11cd9571932f3a8eee285c1c44c"
},
{
"path": "skills/create-odoo-pwa/templates/vue/base/vite.config.js.template",
"sha256": "3a97a9170c8a96568dcbd2eb58c96e48e549f99b4d74c5d6fbe8affdf4ce25d1"
},
{
"path": "skills/create-odoo-pwa/templates/vue/src/App.vue.template",
"sha256": "dc371a60731c01a3ceb208c0129b3bb0aff9884704da4a5f7384cf6afb1b81e7"
},
{
"path": "skills/create-odoo-pwa/templates/vue/src/style.css.template",
"sha256": "991f6f22fdea623349431e7fac4c1936efd7c5d6c506a1819e7818c2cb475420"
},
{
"path": "skills/create-odoo-pwa/templates/vue/src/main.js.template",
"sha256": "8ef1f40fcd7ced0ce98adafc627e7b612994db9977b1c6cec0ac3067a0324c7a"
},
{
"path": "skills/create-odoo-pwa/templates/vue/src/composables/useTaskCache.js.template",
"sha256": "b3a7432895bda00cd3ff155c69ca8303bf4815871dc6be8401d14d59ff8a44d7"
},
{
"path": "skills/create-odoo-pwa/templates/vue/src/components/OfflineBanner.vue.template",
"sha256": "d78bc155fdc9f1092c80719116d9e43d5cea53814a0a107191bf66f78d88277d"
},
{
"path": "skills/create-odoo-pwa/templates/vue/src/api/odoo.js.template",
"sha256": "319c133ea305e1f4beffb2040a7b0ed8b73c9f2d1ca9fe5c5af0383aead985e4"
},
{
"path": "skills/create-odoo-pwa/templates/vue/src/pages/AddPage.vue.template",
"sha256": "466a6e49d6a5f62df3eae5aca06ed1179e5c6c3a9f8fc66cba3c6aaf20e40d75"
},
{
"path": "skills/create-odoo-pwa/templates/vue/src/pages/ListPage.vue.template",
"sha256": "b4edaebaaa80abeeb6ca554f121344d15140223c7148e071a7b1f02c9a5f0c02"
},
{
"path": "skills/create-odoo-pwa/templates/sveltekit/docs/README.md.template",
"sha256": "e4422ca8c0f271120db4fdb098e14e6e7c7e15bb4159c1154e321178419dade6"
},
{
"path": "skills/create-odoo-pwa/templates/sveltekit/docs/CLAUDE.md.template",
"sha256": "a0f12c0f571d948ff4f06d662f3e0896f1a6f60ba78d6ccf6958b83708745011"
},
{
"path": "skills/create-odoo-pwa/templates/sveltekit/static/manifest.json.template",
"sha256": "8f90232388985278249df5a00661e1b14522458db4af314d00e77320e865a624"
},
{
"path": "skills/create-odoo-pwa/templates/sveltekit/deployment/vercel.json.template",
"sha256": "65537c0d3e47092c1347a3924208a96f747205274c384edc97ba9868095caab4"
},
{
"path": "skills/create-odoo-pwa/templates/sveltekit/deployment/github-actions.yml.template",
"sha256": "16f0a42030fe74893cfeffc42f12a499cf62e05c3fa382932d595fa9240b971c"
},
{
"path": "skills/create-odoo-pwa/templates/sveltekit/lib/utils.js.template",
"sha256": "337a6cbda073f0a172f55e4f7d7dbcc56c1a8a837856d95de57431bfeecfdafe"
},
{
"path": "skills/create-odoo-pwa/templates/sveltekit/lib/odoo.js.template",
"sha256": "cbf095ee000861d15a13d5b23a7298a3f0eae53e09c91d3d59d756fc5c1858c8"
},
{
"path": "skills/create-odoo-pwa/templates/sveltekit/lib/db.js.template",
"sha256": "4a00e2dcc2271eef4c565bdf73b85819d50a44ec205788c84d5a9e16be93602c"
},
{
"path": "skills/create-odoo-pwa/templates/sveltekit/lib/stores/cache.js.template",
"sha256": "0292bf52aaac01af04822d154d03eec3b5ea00aaf66eddadb75ddb60481524ee"
},
{
"path": "skills/create-odoo-pwa/templates/sveltekit/routes/list/+page.svelte.template",
"sha256": "0bb94af459fa4760ab7e204f6a662297e8991c806e73da33fa47977770d54a98"
},
{
"path": "skills/create-odoo-pwa/templates/sveltekit/routes/root/+layout.svelte.template",
"sha256": "3fd8d0e4ea5eac6f952699eec2fafe89b207d7ea4006a8fb85c9b9f876145117"
},
{
"path": "skills/create-odoo-pwa/templates/sveltekit/routes/root/+layout.js.template",
"sha256": "f85e6d24b47d64a5f21dc397706ff1e118d23eadb1ab65da8b6ae11e7a1ed672"
},
{
"path": "skills/create-odoo-pwa/templates/sveltekit/routes/root/+page.svelte.template",
"sha256": "29cefe347404b327b55e043017f06a7bf0505452c77636bfc9f7dd0531b89acf"
},
{
"path": "skills/create-odoo-pwa/templates/sveltekit/routes/api/odoo/+server.js.template",
"sha256": "c919559346017e1bbdfdef8d71671e194d0915e998430740177feeee944665ca"
},
{
"path": "skills/create-odoo-pwa/templates/sveltekit/base/jsconfig.json.template",
"sha256": "9b8291d8f3570eee2faf4c3078f465278aa59ca7fbffe042231c4f268cf2beb4"
},
{
"path": "skills/create-odoo-pwa/templates/sveltekit/base/.gitignore.template",
"sha256": "5b738b6a9aef748f85e1ee779fd04d115777e112f7532635d0b06d3b6fb763bf"
},
{
"path": "skills/create-odoo-pwa/templates/sveltekit/base/.env.example.template",
"sha256": "1d3c65be859aba390caccc4434f869a8a3d9024de4b04ca8d831e008ab274d64"
},
{
"path": "skills/create-odoo-pwa/templates/sveltekit/base/package.json.template",
"sha256": "7e360709bf19cb20f1f74c6edec053394e705b00a65f49de1bce8c9435f2ee6a"
},
{
"path": "skills/create-odoo-pwa/templates/sveltekit/base/svelte.config.js.template",
"sha256": "d6b70c65b52e045ea9e034e5e81f7dbbc8461e802fcb366a4dd5f2d6f78801f9"
},
{
"path": "skills/create-odoo-pwa/templates/sveltekit/base/vite.config.js.template",
"sha256": "c34481be0af763f357c2177b794e52b773ffa13589b8fc46ea98e172862427a1"
},
{
"path": "skills/create-odoo-pwa/templates/react/base/index.html.template",
"sha256": "5bb0a55917570737010e6411ebd5981abab1ca87e0c5ae505658165b8889974a"
},
{
"path": "skills/create-odoo-pwa/templates/react/base/.gitignore.template",
"sha256": "4b32e84abb8c51d6f119b494f0f760b85c754686920b731a74dbac46262c9975"
},
{
"path": "skills/create-odoo-pwa/templates/react/base/.env.example.template",
"sha256": "a37594d8960fb1274c8163c25b285e7e6530a43f65ff8035805196167eec8be2"
},
{
"path": "skills/create-odoo-pwa/templates/react/base/package.json.template",
"sha256": "fdb850ea90165735240548675cb98649f10ed2c4c6926a72b0ee710554fee7cc"
},
{
"path": "skills/create-odoo-pwa/templates/react/base/vite.config.js.template",
"sha256": "0817516a855a3dce4f27d99dbbde48f8e7a8de85033c8703d160addc25a5118c"
},
{
"path": "skills/create-odoo-pwa/templates/react/src/App.css.template",
"sha256": "2ddbf5fdd9d6fc317ceb6b79e135e92baa19c20b3b071b014ef0b581af07948b"
},
{
"path": "skills/create-odoo-pwa/templates/react/src/main.jsx.template",
"sha256": "5e6a57f0d1a6a2c4165d99d69545a0a63c20f3da541c913afac249922dda4771"
},
{
"path": "skills/create-odoo-pwa/templates/react/src/index.css.template",
"sha256": "370b1bc983221cdbf0f1c7930c919dc89353f37991b7468e4014fb6365991b0a"
},
{
"path": "skills/create-odoo-pwa/templates/react/src/App.jsx.template",
"sha256": "3358e0f975acf703fd5387678d39e6495e148c94bb3fc87837df51ea71d5b5aa"
},
{
"path": "skills/create-odoo-pwa/templates/react/src/context/TaskContext.jsx.template",
"sha256": "6691f5c18ea894ceddfa03bc38306f466f2d31b8c3b931ce97316f43ca4f0e1f"
},
{
"path": "skills/create-odoo-pwa/templates/react/src/components/OfflineBanner.jsx.template",
"sha256": "3a6c980142d8173898759450529244d321c2b9550c6fda52fca94e9d264b48ab"
},
{
"path": "skills/create-odoo-pwa/templates/react/src/api/odoo.js.template",
"sha256": "319c133ea305e1f4beffb2040a7b0ed8b73c9f2d1ca9fe5c5af0383aead985e4"
},
{
"path": "skills/create-odoo-pwa/templates/react/src/pages/ListPage.jsx.template",
"sha256": "3e63a08e04d2eed445028b4fb16195b24f384339757835a6166e5cb5db15d5b3"
},
{
"path": "skills/create-odoo-pwa/templates/react/src/pages/AddPage.jsx.template",
"sha256": "f94e5683647e7189751843ef5e4a4181536645ad5d93a0087e4d2261ccf76004"
}
],
"dirSha256": "9b11048764766904a04057b43ba25a1e814d8ba8cc0ba6a060d2a5fa3f091d7b"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,204 @@
---
name: add-odoo-model
description: Add integration for an additional Odoo Studio model to an existing Odoo PWA project. Use when user wants to add support for another model, mentions "add new model", "integrate another Odoo model", or similar.
allowed-tools: Read, Write, Edit, Glob
---
# Add Odoo Model Integration
Add a new Odoo model integration to an existing Odoo PWA project, creating cache stores, API methods, and UI components.
## Prerequisites
- Existing Odoo PWA project (generated with create-odoo-pwa skill)
- New Odoo Studio model created with `x_` prefix
- Model name and display name from user
## Required User Input
Ask the user for:
1. **Model name** (required)
- Format: without `x_` prefix (e.g., "inventory", "tasks")
- Example: If Odoo model is `x_inventory`, user provides: `inventory`
2. **Model display name** (required)
- Human-readable singular name (e.g., "Inventory Item", "Task")
3. **Create UI pages** (optional)
- Ask if user wants to generate form and list pages
- Default: yes
## Detection Steps
Before generating, detect the project structure:
1. **Detect framework**:
- Check for `svelte.config.js` → SvelteKit
- Check for `vite.config.ts` with React → React
- Check for `nuxt.config.ts` → Vue/Nuxt
2. **Find existing files**:
- Locate `src/lib/odoo.js` (or equivalent)
- Find existing cache stores in `src/lib/stores/`
- Check routes structure
3. **Verify Odoo connection**:
- Check `.env` file has ODOO_URL and credentials
## Generation Steps
### Step 1: Create Cache Store
Generate `src/lib/stores/{{MODEL_NAME}}Cache.js`:
- Based on existing cache store pattern
- Replace model name throughout
- Update fields array with model-specific fields
- Include CRUD methods
### Step 2: Update Odoo API Client
Add model-specific methods to `src/lib/odoo.js`:
```javascript
/**
* Fetch {{MODEL_DISPLAY_NAME}} records
*/
async fetch{{MODEL_NAME|capitalize}}s(domain = [], fields = []) {
return await this.searchRecords('x_{{MODEL_NAME}}', domain, fields);
}
/**
* Create {{MODEL_DISPLAY_NAME}}
*/
async create{{MODEL_NAME|capitalize}}(fields) {
return await this.createRecord('x_{{MODEL_NAME}}', fields);
}
/**
* Update {{MODEL_DISPLAY_NAME}}
*/
async update{{MODEL_NAME|capitalize}}(id, values) {
return await this.updateRecord('x_{{MODEL_NAME}}', id, values);
}
/**
* Delete {{MODEL_DISPLAY_NAME}}
*/
async delete{{MODEL_NAME|capitalize}}(id) {
return await this.deleteRecord('x_{{MODEL_NAME}}', id);
}
```
### Step 3: Create UI Pages (if requested)
#### Add Form Page: `src/routes/{{MODEL_NAME}}/+page.svelte`
Generate form component:
- Import cache store
- Form fields for model
- Handle offline/online states
- Submit handler with validation
#### List Page: `src/routes/{{MODEL_NAME}}/list/+page.svelte`
Generate list component:
- Display records in table/card format
- Search/filter functionality
- Delete actions
- Sync status
### Step 4: Update Navigation
Update navigation in main layout or existing pages:
```svelte
<nav>
<!-- Existing links -->
<a href="/{{MODEL_NAME}}">{{MODEL_DISPLAY_NAME}}s</a>
</nav>
```
### Step 5: Update Environment Variables
Add to `.env.example` (if needed):
```env
# {{MODEL_DISPLAY_NAME}} Model
ODOO_{{MODEL_NAME|uppercase}}_MODEL=x_{{MODEL_NAME}}
```
## Post-Generation Instructions
Provide user with:
```
✅ {{MODEL_DISPLAY_NAME}} integration added successfully!
📋 Next Steps:
1. Verify Odoo Model Setup:
- Model name: x_{{MODEL_NAME}}
- Add custom fields with x_studio_ prefix in Odoo Studio
2. Update Cache Store:
- Edit src/lib/stores/{{MODEL_NAME}}Cache.js
- Add all model fields to the 'fields' array
3. Customize UI:
- Edit src/routes/{{MODEL_NAME}}/+page.svelte for form
- Edit src/routes/{{MODEL_NAME}}/list/+page.svelte for list view
- Add model-specific fields and validation
4. Test Integration:
npm run dev
- Navigate to /{{MODEL_NAME}}
- Test create, read, update, delete operations
- Verify offline functionality
📚 Model-Specific Files Created:
- src/lib/stores/{{MODEL_NAME}}Cache.js - Cache and sync logic
- src/routes/{{MODEL_NAME}}/+page.svelte - Add form
- src/routes/{{MODEL_NAME}}/list/+page.svelte - List view
🔗 Access:
- Add: http://localhost:5173/{{MODEL_NAME}}
- List: http://localhost:5173/{{MODEL_NAME}}/list
```
## Framework-Specific Notes
### SvelteKit
- Use Svelte 5 syntax with `$state`, `$derived`, `$effect`
- Cache stores use Svelte stores pattern
- Routes in `src/routes/`
### React
- Use React hooks (useState, useEffect)
- Context API for cache
- Routes configuration depends on router (React Router, etc.)
### Vue
- Use Vue 3 Composition API
- Composables for cache logic
- Routes in `src/pages/` or as configured
## Error Handling
If generation fails:
- Verify project has Odoo PWA structure
- Check for existing odoo.js file
- Ensure proper permissions for file creation
- Provide clear error messages
## Examples
User: "Add inventory model to track items"
- Model name: inventory
- Display name: Inventory Item
- Creates: inventoryCache.js, /inventory pages, API methods
User: "Integrate task management"
- Model name: task
- Display name: Task
- Creates: taskCache.js, /task pages, API methods

View File

@@ -0,0 +1,411 @@
---
name: create-odoo-pwa
description: Generate an offline-first Progressive Web App with Odoo Studio backend integration. Use when user wants to create new Odoo-backed application, mentions "PWA with Odoo", "offline Odoo app", "Odoo Studio PWA", or similar terms. Supports SvelteKit, React, and Vue frameworks.
allowed-tools: Read, Write, Glob, Bash
---
# Create Odoo PWA Application
Generate a production-ready Progressive Web App with Odoo Studio backend, featuring offline-first architecture, smart caching, and automatic synchronization.
## Before You Start
This skill generates a complete PWA project following proven architectural patterns:
- **Three-layer data flow**: Component → Cache Store → API Client → Server Route → Odoo
- **Offline-first**: IndexedDB/localStorage with background sync
- **Smart caching**: Incremental fetch, stale detection, optimistic updates
- **PWA-ready**: Service workers, manifest, installable
## Required User Input
Ask the user for the following information before generating:
1. **Project name** (required)
- Format: kebab-case (e.g., "inventory-tracker", "expense-manager")
- Used for directory name and package.json
2. **Framework** (required)
- Options: `sveltekit` (recommended), `react`, `vue`
- Default: sveltekit if not specified
3. **Primary Odoo model** (required)
- The main custom model name WITHOUT the `x_` prefix
- Example: If Odoo model is `x_inventory`, user provides: `inventory`
- Will automatically add `x_` prefix in code
4. **Model display name** (required)
- Human-readable singular name (e.g., "Inventory Item", "Expense")
5. **Deployment target** (optional)
- Options: `vercel`, `github-pages`, `cloudflare`, `netlify`
- Default: vercel if not specified
## Generation Steps
### Step 1: Project Initialization
Create the project directory and initialize the structure:
```bash
mkdir {{PROJECT_NAME}}
cd {{PROJECT_NAME}}
```
Generate the appropriate structure based on framework:
- **SvelteKit**: Use SvelteKit 2.x structure with `src/` directory
- **React**: Use Vite + React structure
- **Vue**: Use Vite + Vue structure
### Step 2: Base Configuration Files
Generate these files using templates from `skills/create-odoo-pwa/templates/{{FRAMEWORK}}/base/`:
#### For SvelteKit:
- `package.json` - Dependencies including @sveltejs/kit, @vite-pwa/sveltekit, @sveltejs/adapter-static
- `svelte.config.js` - SvelteKit configuration with adapter-static
- `vite.config.js` - Vite + PWA plugin configuration
- `jsconfig.json` or `tsconfig.json` - Path aliases and compiler options
#### For React:
- `package.json` - Dependencies including React 18, Vite, vite-plugin-pwa
- `vite.config.js` - React + PWA plugin configuration
- `tsconfig.json` - TypeScript configuration
#### For Vue:
- `package.json` - Dependencies including Vue 3, Vite, vite-plugin-pwa
- `vite.config.js` - Vue + PWA plugin configuration
- `tsconfig.json` - TypeScript configuration
### Step 3: Environment and Git Configuration
Create `.env.example`:
```env
# Odoo Instance Configuration
ODOO_URL=https://your-instance.odoo.com
ODOO_DB=your-database-name
ODOO_USERNAME=your-username
ODOO_API_KEY=your-api-key
# Primary Model (use x_ prefix)
ODOO_PRIMARY_MODEL=x_{{MODEL_NAME}}
# Optional: For static hosting (GitHub Pages, etc.)
PUBLIC_API_URL=
```
Create `.gitignore`:
```
node_modules/
.env
dist/
build/
.svelte-kit/
.vercel/
.DS_Store
*.log
```
### Step 4: Core Library Files
Generate these essential files from templates:
#### A. Odoo API Client (`src/lib/odoo.js`)
Features:
- API URL configuration (supports PUBLIC_API_URL for static hosts)
- `callApi(action, data)` - Core API communication
- `createRecord(model, fields)` - Create records
- `searchRecords(model, domain, fields)` - Search/read records
- `updateRecord(model, id, values)` - Update records
- `deleteRecord(model, id)` - Delete records
- `formatMany2one(id)` - Format single relation fields
- `formatMany2many(ids)` - Format multi-relation fields
- Model-specific convenience methods
#### B. IndexedDB Manager (`src/lib/db.js`)
Features:
- Database initialization with versioning
- Store definitions for master data (partners, categories, config)
- CRUD operations: `add()`, `get()`, `getAll()`, `update()`, `remove()`
- Transaction helpers
- Error handling
#### C. Smart Cache Store (`src/lib/stores/{{MODEL_NAME}}Cache.js`)
Features:
- Framework-specific store pattern (Svelte store/React context/Vue composable)
- Dual storage strategy (localStorage for metadata, IndexedDB for master data)
- `initialize()` - Load cache and start background sync
- `sync(forceFullRefresh)` - Incremental sync with Odoo
- `forceRefresh()` - Clear cache and full sync
- Partner name resolution with caching
- Optimistic UI updates
- Stale detection (5-minute cache validity)
- Background sync (3-minute intervals)
- Derived stores for UI states
#### D. Utility Functions (`src/lib/{{MODEL_NAME}}Utils.js`)
Features:
- Business logic calculations
- Data normalization helpers
- Field formatters
### Step 5: Server-Side API Proxy
#### For SvelteKit: `src/routes/api/odoo/+server.js`
Features:
- JSON-RPC client for Odoo
- UID caching to reduce auth calls
- `execute(model, method, args, kwargs)` helper
- POST request handler with actions:
- `create` - Create new record
- `search` - Search and read records
- `search_model` - Search any Odoo model
- `update` - Update existing record
- `delete` - Delete record
- Error handling with descriptive messages
- Environment variable access
#### For React/Vue: `src/api/odoo.js` (server endpoint)
Similar functionality adapted to framework conventions
### Step 6: UI Components and Routes
Generate starter components and pages:
#### SvelteKit Routes:
- `src/routes/+layout.svelte` - Root layout with navigation
- `src/routes/+layout.js` - SSR/CSR configuration (ssr: false, csr: true)
- `src/routes/+page.svelte` - Main form for creating records
- `src/routes/list/+page.svelte` - List/table view with filtering
- `src/app.html` - HTML template with PWA meta tags
#### React/Vue Pages:
Equivalent component structure adapted to framework conventions
#### Shared Components:
- `OfflineBanner` - Shows online/offline status
- `SyncStatus` - Displays sync state and last sync time
- `LoadingSpinner` - Loading indicator
- Form components with offline support
### Step 7: PWA Configuration
Generate PWA files:
#### `static/manifest.json` (or `public/manifest.json`):
```json
{
"name": "{{PROJECT_NAME}}",
"short_name": "{{PROJECT_NAME}}",
"description": "{{MODEL_DISPLAY_NAME}} management app",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#667eea",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
```
Configure service worker in `vite.config.js`:
- Auto-update strategy
- Cache all static assets
- Offline support
### Step 8: Deployment Configuration
Generate deployment files based on target:
#### Vercel (`vercel.json`):
```json
{
"buildCommand": "npm run build",
"outputDirectory": "build",
"framework": "sveltekit",
"regions": ["iad1"]
}
```
#### GitHub Pages (`.github/workflows/deploy.yml`):
```yaml
name: Deploy to GitHub Pages
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npm run build
- uses: actions/upload-pages-artifact@v2
with:
path: build
```
#### Cloudflare/Netlify:
Generate appropriate configuration files
### Step 9: Documentation
Generate comprehensive documentation:
#### `README.md`:
- Project overview
- Prerequisites (Node.js, Odoo account)
- Installation steps
- Odoo Studio model setup instructions
- Development commands
- Deployment guide
- Architecture overview
#### `CLAUDE.md`:
Complete architecture documentation following the expense-split-pwa pattern:
- Project overview
- Development commands
- Environment setup
- Architecture diagrams
- Key architectural patterns
- Odoo model structure
- Important development notes
- Common gotchas
#### `API.md`:
- Odoo integration patterns
- Available API methods
- Field formatting examples
- Common operations
### Step 10: Post-Generation Instructions
After generating all files, provide the user with:
```
✅ Project '{{PROJECT_NAME}}' generated successfully!
📋 Next Steps:
1. Navigate to the project:
cd {{PROJECT_NAME}}
2. Install dependencies:
npm install
3. Configure Odoo credentials:
cp .env.example .env
# Edit .env with your Odoo instance details
4. Create Odoo Studio Model:
- Log into your Odoo instance
- Go to Studio
- Create a new model named: x_{{MODEL_NAME}}
- Add custom fields with x_studio_ prefix
- Example fields:
* x_name (Char) - Required
* x_studio_description (Text)
* x_studio_value (Float)
* x_studio_date (Date)
* x_studio_category (Many2one → res.partner or custom)
5. Start development server:
npm run dev
6. Generate PWA icons:
- Create 192x192 and 512x512 PNG icons
- Place in static/ or public/ directory
- Name them icon-192.png and icon-512.png
7. Deploy (optional):
- Vercel: vercel
- GitHub Pages: Push to main branch
- Cloudflare: wrangler deploy
- Netlify: netlify deploy
📚 Documentation:
- README.md - Getting started guide
- CLAUDE.md - Architecture documentation
- API.md - Odoo integration patterns
🔗 Resources:
- Odoo API Docs: https://www.odoo.com/documentation/
- SvelteKit Docs: https://kit.svelte.dev/
```
## Template Variables
When generating files, replace these placeholders:
- `{{PROJECT_NAME}}` - User's project name (kebab-case)
- `{{MODEL_NAME}}` - Odoo model name without x_ prefix
- `{{MODEL_DISPLAY_NAME}}` - Human-readable model name
- `{{FRAMEWORK}}` - sveltekit/react/vue
- `{{DEPLOYMENT_TARGET}}` - vercel/github-pages/cloudflare/netlify
- `{{AUTHOR_NAME}}` - User's name (if provided)
## Common Patterns from expense-split-pwa
This skill implements proven patterns from the expense-split-pwa project:
1. **Smart Caching**: Load from cache immediately, sync in background if stale
2. **Incremental Fetch**: Only fetch records with `id > lastRecordId`
3. **Partner Resolution**: Batch-fetch and cache partner names
4. **Dual-Phase Calculation**: Process settled/unsettled records separately
5. **Optimistic Updates**: Update UI immediately, sync to server in background
6. **Error Recovery**: Graceful degradation when offline
## Error Handling
If generation fails:
- Verify all required input is provided
- Check template files exist
- Ensure proper permissions for file creation
- Provide clear error messages to user
## Framework-Specific Notes
### SvelteKit
- Use Svelte 5 runes syntax (`$state`, `$derived`, `$effect`)
- Configure adapter-static for static deployment
- Set `ssr: false` for client-side only apps
- Use `$app/paths` for base path support
### React
- Use React 18+ with hooks
- Context API for global state
- React Query for server state (optional)
- Vite for build tooling
### Vue
- Use Vue 3 Composition API
- Pinia for state management
- Composables for reusable logic
- Vite for build tooling
## Testing the Generated Project
After generation, verify:
1. `npm install` completes without errors
2. `npm run dev` starts development server
3. Form renders correctly
4. Offline banner appears when disconnecting
5. Data persists in localStorage/IndexedDB
6. Sync works when back online
## Support
For issues or questions:
- Check CLAUDE.md for architecture details
- Review generated code comments
- Consult Odoo API documentation
- Verify environment variables are set correctly

View File

@@ -0,0 +1,11 @@
# Odoo Instance Configuration (for backend server)
ODOO_URL=https://your-instance.odoo.com
ODOO_DB=your-database-name
ODOO_USERNAME=your-username
ODOO_API_KEY=your-api-key
# Primary Model (use x_ prefix for Odoo Studio models)
ODOO_PRIMARY_MODEL=x_{{MODEL_NAME}}
# Frontend: API endpoint (if frontend and backend are separate)
VITE_API_URL=http://localhost:3000

View File

@@ -0,0 +1,29 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Environment
.env
.env.local
.env.production

View File

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#667eea" />
<meta name="description" content="{{MODEL_DISPLAY_NAME}} management PWA with Odoo integration" />
<link rel="manifest" href="/manifest.json" />
<title>{{PROJECT_NAME}}</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

View File

@@ -0,0 +1,28 @@
{
"name": "{{PROJECT_NAME}}",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.0"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.2",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.7",
"vite": "^5.3.4",
"vite-plugin-pwa": "^0.20.0"
}
}

View File

@@ -0,0 +1,48 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,json,webp}']
},
manifest: {
name: '{{PROJECT_NAME}}',
short_name: '{{PROJECT_NAME}}',
description: 'PWA for {{MODEL_DISPLAY_NAME}} management with Odoo integration',
theme_color: '#667eea',
background_color: '#ffffff',
display: 'standalone',
icons: [
{
src: '/icon-192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: '/icon-512.png',
sizes: '512x512',
type: 'image/png'
}
]
}
})
],
resolve: {
alias: {
'@': '/src'
}
},
server: {
proxy: {
'/api': {
target: process.env.VITE_API_URL || 'http://localhost:3000',
changeOrigin: true
}
}
}
});

View File

@@ -0,0 +1,311 @@
:root {
--primary-color: #667eea;
--primary-dark: #5568d3;
--bg-gradient-start: #667eea;
--bg-gradient-end: #764ba2;
--error-bg: #f8d7da;
--error-text: #721c24;
--success-bg: #d4edda;
--success-text: #155724;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu,
Cantarell, 'Helvetica Neue', sans-serif;
background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
min-height: 100vh;
}
.app {
min-height: 100vh;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 16px;
}
h1 {
color: white;
text-align: center;
margin-bottom: 30px;
font-size: 2.5em;
}
h2 {
margin: 0;
color: #333;
font-size: 1.5em;
}
.nav {
display: flex;
gap: 10px;
margin-bottom: 30px;
background: white;
border-radius: 10px;
padding: 5px;
}
.nav a {
flex: 1;
text-align: center;
padding: 12px;
text-decoration: none;
color: var(--primary-color);
border-radius: 8px;
font-weight: 600;
transition: all 0.3s;
}
.nav a.active {
background: var(--primary-color);
color: white;
}
.offline-banner {
background: #e3f2fd;
color: #1565c0;
padding: 12px 20px;
border-radius: 10px;
margin-bottom: 20px;
text-align: center;
font-weight: 600;
border: 2px solid #64b5f6;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; }
}
.form, .list-container {
background: white;
padding: 24px;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
}
input {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
}
input:focus {
outline: none;
border-color: var(--primary-color);
}
.button-group {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
flex: 1;
min-width: 150px;
padding: 15px;
background: var(--primary-color);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
button:hover:not(:disabled) {
background: var(--primary-dark);
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.refresh-btn {
background: #f0f0f0;
color: var(--primary-color);
flex: 0 1 auto;
}
.refresh-btn:hover:not(:disabled) {
background: #e0e0e0;
color: var(--primary-dark);
}
.message {
padding: 12px;
border-radius: 8px;
margin-bottom: 15px;
background: var(--success-bg);
color: var(--success-text);
text-align: center;
}
.message.error {
background: var(--error-bg);
color: var(--error-text);
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 15px;
}
.header-actions {
display: flex;
gap: 10px;
align-items: center;
}
.search-input {
padding: 10px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
min-width: 200px;
}
.refresh-btn-small {
padding: 10px 15px;
background: #f0f0f0;
color: var(--primary-color);
border: none;
border-radius: 8px;
font-size: 18px;
cursor: pointer;
transition: all 0.3s;
min-width: auto;
flex: 0;
}
.refresh-btn-small:hover:not(:disabled) {
background: #e0e0e0;
transform: rotate(360deg);
}
.loading, .empty {
text-align: center;
padding: 40px;
color: #666;
font-size: 16px;
}
.record-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.record-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border: 2px solid #e0e0e0;
border-radius: 10px;
transition: all 0.3s;
}
.record-card:hover {
border-color: var(--primary-color);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1);
}
.record-content {
flex: 1;
}
.record-content h3 {
margin: 0 0 8px 0;
color: #333;
font-size: 1.1em;
}
.record-meta {
margin: 0;
color: #666;
font-size: 0.9em;
}
.record-actions {
display: flex;
gap: 8px;
}
.delete-btn {
padding: 8px 12px;
background: #fff5f5;
color: #e53e3e;
border: 1px solid #feb2b2;
border-radius: 6px;
font-size: 18px;
cursor: pointer;
transition: all 0.3s;
min-width: auto;
flex: 0;
}
.delete-btn:hover {
background: #fed7d7;
transform: none;
box-shadow: none;
}
@media (max-width: 600px) {
.list-header {
flex-direction: column;
align-items: stretch;
}
.header-actions {
flex-direction: column;
align-items: stretch;
}
.search-input {
min-width: auto;
}
.record-card {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.record-actions {
align-self: flex-end;
}
}

View File

@@ -0,0 +1,41 @@
import { Routes, Route, Link, useLocation } from 'react-router-dom';
import AddPage from './pages/AddPage';
import ListPage from './pages/ListPage';
import OfflineBanner from './components/OfflineBanner';
import './App.css';
function App() {
const location = useLocation();
return (
<div className="app">
<div className="container">
<h1>📋 {{PROJECT_NAME}}</h1>
<OfflineBanner />
<nav className="nav">
<Link
to="/"
className={location.pathname === '/' ? 'active' : ''}
>
Add {{MODEL_DISPLAY_NAME}}
</Link>
<Link
to="/list"
className={location.pathname === '/list' ? 'active' : ''}
>
View All
</Link>
</nav>
<Routes>
<Route path="/" element={<AddPage />} />
<Route path="/list" element={<ListPage />} />
</Routes>
</div>
</div>
);
}
export default App;

View File

@@ -0,0 +1,123 @@
/**
* Odoo API Client for React
* Communicates with backend server that proxies to Odoo
*/
class OdooAPI {
constructor() {
this.apiUrl = import.meta.env.VITE_API_URL
? `${import.meta.env.VITE_API_URL}/api/odoo`
: '/api/odoo';
}
/**
* Call the server-side API
* @param {string} action
* @param {any} data
* @returns {Promise<any>}
*/
async callApi(action, data) {
const response = await fetch(this.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ action, data })
});
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'API Error');
}
return result;
}
/**
* Create a new record in specified model
* @param {string} model - Odoo model name
* @param {Record<string, any>} fields - Record fields
* @returns {Promise<number>} - Created record ID
*/
async createRecord(model, fields) {
const result = await this.callApi('create', { model, fields });
return result.id;
}
/**
* Search and read records from specified model
* @param {string} model - Odoo model name
* @param {any[]} domain - Odoo domain filter
* @param {string[]} fields - Fields to retrieve
* @returns {Promise<any[]>} - Array of records
*/
async searchRecords(model, domain = [], fields = []) {
const result = await this.callApi('search', { model, domain, fields });
return result.results;
}
/**
* Generic search_read for any model
* @param {string} model
* @param {any[]} domain
* @param {string[]} fields
* @returns {Promise<any[]>}
*/
async searchModel(model, domain = [], fields = []) {
const result = await this.callApi('search_model', { model, domain, fields });
return result.results;
}
/**
* Fetch partner list (common operation)
* @returns {Promise<Array<{id:number, display_name:string}>>}
*/
async fetchPartners() {
return await this.searchModel('res.partner', [], ['id', 'display_name']);
}
/**
* Format a many2one field value
* @param {number|string|null|undefined} id
* @returns {number|false}
*/
formatMany2one(id) {
return id ? Number(id) : false;
}
/**
* Format a many2many field using the (6,0,[ids]) command
* @param {Array<number|string>} ids
* @returns {any[]}
*/
formatMany2many(ids) {
if (!Array.isArray(ids) || ids.length === 0) return [];
return [[6, 0, ids.map((i) => Number(i))]];
}
/**
* Update a record
* @param {string} model - Odoo model name
* @param {number} id - Record ID
* @param {Record<string, any>} values - Fields to update
* @returns {Promise<boolean>}
*/
async updateRecord(model, id, values) {
const result = await this.callApi('update', { model, id, values });
return result.result;
}
/**
* Delete a record
* @param {string} model - Odoo model name
* @param {number} id - Record ID
* @returns {Promise<boolean>}
*/
async deleteRecord(model, id) {
const result = await this.callApi('delete', { model, id });
return result.result;
}
}
export const odooClient = new OdooAPI();

View File

@@ -0,0 +1,28 @@
import { useState, useEffect } from 'react';
function OfflineBanner() {
const [isOffline, setIsOffline] = useState(!navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOffline(false);
const handleOffline = () => setIsOffline(true);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
if (!isOffline) return null;
return (
<div className="offline-banner">
📡 Offline Mode - Data will be synced when you're back online
</div>
);
}
export default OfflineBanner;

View File

@@ -0,0 +1,243 @@
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { odooClient } from '../api/odoo';
const TaskContext = createContext();
// Cache configuration
const CACHE_KEY = '{{MODEL_NAME}}_cache_v1';
const CACHE_META_KEY = '{{MODEL_NAME}}_cache_meta_v1';
const CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes cache validity
const SYNC_INTERVAL_MS = 3 * 60 * 1000; // Background sync every 3 minutes
// Helper functions for localStorage
function loadFromStorage() {
try {
const cachedData = localStorage.getItem(CACHE_KEY);
const meta = localStorage.getItem(CACHE_META_KEY);
if (cachedData && meta) {
const records = JSON.parse(cachedData);
const metaData = JSON.parse(meta);
const now = Date.now();
const isStale = now - metaData.lastSyncTime > CACHE_DURATION_MS;
return { records, meta: { ...metaData, isStale } };
}
} catch (e) {
console.warn('Failed to load cache from storage:', e);
}
return {
records: [],
meta: {
lastSyncTime: 0,
lastRecordId: 0,
recordCount: 0,
isStale: true
}
};
}
function saveToStorage(records, meta) {
try {
localStorage.setItem(CACHE_KEY, JSON.stringify(records));
localStorage.setItem(CACHE_META_KEY, JSON.stringify({
lastSyncTime: meta.lastSyncTime || Date.now(),
lastRecordId: meta.lastRecordId || 0,
recordCount: records.length,
isStale: false
}));
} catch (e) {
console.warn('Failed to save cache to storage:', e);
}
}
export function TaskProvider({ children }) {
const [records, setRecords] = useState([]);
const [loading, setLoading] = useState(false);
const [syncing, setSyncing] = useState(false);
const [error, setError] = useState('');
const [meta, setMeta] = useState({
lastSyncTime: 0,
lastRecordId: 0,
recordCount: 0,
isStale: true
});
// Sync function - fetches new data from server
const sync = useCallback(async (forceFullRefresh = false) => {
setSyncing(true);
setError('');
try {
const fields = ['id', 'x_name'];
let domain = [];
let fetchedRecords = [];
if (!forceFullRefresh && meta.lastRecordId > 0) {
// Incremental fetch
domain = [['id', '>', meta.lastRecordId]];
try {
fetchedRecords = await odooClient.searchRecords('x_{{MODEL_NAME}}', domain, fields);
} catch (err) {
console.warn('Incremental fetch failed, falling back to full fetch:', err);
forceFullRefresh = true;
}
}
if (forceFullRefresh || meta.lastRecordId === 0) {
// Full refresh
fetchedRecords = await odooClient.searchRecords('x_{{MODEL_NAME}}', [], fields);
}
// Merge or replace records
let mergedRecords;
if (forceFullRefresh || meta.lastRecordId === 0) {
mergedRecords = fetchedRecords;
} else {
const existingIds = new Set(records.map(r => r.id));
const newRecords = fetchedRecords.filter(r => !existingIds.has(r.id));
mergedRecords = [...records, ...newRecords];
}
// Sort by ID
mergedRecords.sort((a, b) => a.id - b.id);
// Calculate new metadata
const lastRecordId = mergedRecords.length > 0
? Math.max(...mergedRecords.map(r => r.id))
: 0;
const newMeta = {
lastSyncTime: Date.now(),
lastRecordId,
recordCount: mergedRecords.length,
isStale: false
};
// Save to storage
saveToStorage(mergedRecords, newMeta);
// Update state
setRecords(mergedRecords);
setMeta(newMeta);
} catch (error) {
console.error('Sync failed:', error);
setError(error.message || 'Failed to sync data');
} finally {
setSyncing(false);
}
}, [meta.lastRecordId, records]);
// Initialize - load cache and start background sync
useEffect(() => {
const cachedState = loadFromStorage();
if (cachedState.records.length > 0) {
setRecords(cachedState.records);
setMeta(cachedState.meta);
// Sync in background if stale
if (cachedState.meta.isStale) {
sync();
}
} else {
setLoading(true);
sync(true).finally(() => setLoading(false));
}
// Set up periodic background sync
const syncInterval = setInterval(() => {
sync();
}, SYNC_INTERVAL_MS);
return () => clearInterval(syncInterval);
}, []);
// Force refresh
const forceRefresh = useCallback(async () => {
await sync(true);
}, [sync]);
// Create record
const createRecord = useCallback(async (fields) => {
try {
const id = await odooClient.createRecord('x_{{MODEL_NAME}}', fields);
// Optimistically add to cache
const newRecord = { id, ...fields };
setRecords(prev => [...prev, newRecord]);
// Sync to get full record
await sync();
return id;
} catch (error) {
console.error('Failed to create record:', error);
throw error;
}
}, [sync]);
// Update record
const updateRecord = useCallback(async (id, values) => {
try {
await odooClient.updateRecord('x_{{MODEL_NAME}}', id, values);
// Optimistically update cache
setRecords(prev =>
prev.map(r => r.id === id ? { ...r, ...values } : r)
);
// Sync to get full updated record
await sync();
return true;
} catch (error) {
console.error('Failed to update record:', error);
throw error;
}
}, [sync]);
// Delete record
const deleteRecord = useCallback(async (id) => {
try {
await odooClient.deleteRecord('x_{{MODEL_NAME}}', id);
// Optimistically remove from cache
setRecords(prev => prev.filter(r => r.id !== id));
return true;
} catch (error) {
console.error('Failed to delete record:', error);
throw error;
}
}, []);
const value = {
records,
loading,
syncing,
error,
meta,
sync,
forceRefresh,
createRecord,
updateRecord,
deleteRecord
};
return (
<TaskContext.Provider value={value}>
{children}
</TaskContext.Provider>
);
}
export function useTask() {
const context = useContext(TaskContext);
if (!context) {
throw new Error('useTask must be used within TaskProvider');
}
return context;
}

View File

@@ -0,0 +1,26 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
#root {
width: 100%;
}

View File

@@ -0,0 +1,16 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import { TaskProvider } from './context/TaskContext';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BrowserRouter>
<TaskProvider>
<App />
</TaskProvider>
</BrowserRouter>
</React.StrictMode>
);

View File

@@ -0,0 +1,78 @@
import { useState } from 'react';
import { useTask } from '../context/TaskContext';
function AddPage() {
const [name, setName] = useState('');
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const { createRecord, forceRefresh } = useTask();
const handleSubmit = async (e) => {
e.preventDefault();
if (!name.trim()) {
setMessage('⚠️ Please fill in the name field');
return;
}
setLoading(true);
setMessage('');
try {
const payload = { x_name: name };
await createRecord(payload);
if (navigator.onLine) {
setMessage('✅ {{MODEL_DISPLAY_NAME}} added successfully!');
} else {
setMessage('✅ {{MODEL_DISPLAY_NAME}} saved locally! Will sync when online.');
}
// Reset form
setName('');
} catch (error) {
setMessage(`❌ Error: ${error.message}`);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} className="form">
<div className="form-group">
<label htmlFor="name">Name</label>
<input
type="text"
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter {{MODEL_DISPLAY_NAME}} name"
required
/>
</div>
{message && (
<div className={`message ${message.includes('❌') ? 'error' : ''}`}>
{message}
</div>
)}
<div className="button-group">
<button type="submit" disabled={loading}>
{loading ? '⏳ Adding...' : ' Add {{MODEL_DISPLAY_NAME}}'}
</button>
{navigator.onLine && (
<button
type="button"
className="refresh-btn"
onClick={forceRefresh}
>
🔄 Refresh Data
</button>
)}
</div>
</form>
);
}
export default AddPage;

View File

@@ -0,0 +1,76 @@
import { useState } from 'react';
import { useTask } from '../context/TaskContext';
function ListPage() {
const [searchTerm, setSearchTerm] = useState('');
const { records, loading, syncing, forceRefresh, deleteRecord } = useTask();
const filteredRecords = records.filter(record =>
record.x_name?.toLowerCase().includes(searchTerm.toLowerCase())
);
const handleDelete = async (id) => {
if (!confirm('Are you sure you want to delete this {{MODEL_DISPLAY_NAME}}?')) {
return;
}
try {
await deleteRecord(id);
} catch (error) {
alert(`Failed to delete: ${error.message}`);
}
};
return (
<div className="list-container">
<div className="list-header">
<h2>All {{MODEL_DISPLAY_NAME}}s</h2>
<div className="header-actions">
<input
type="search"
placeholder="Search..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
<button
className="refresh-btn-small"
onClick={forceRefresh}
disabled={syncing}
>
{syncing ? '⏳' : '🔄'}
</button>
</div>
</div>
{loading ? (
<div className="loading">Loading...</div>
) : filteredRecords.length === 0 ? (
<div className="empty">
{searchTerm ? 'No matching records found' : 'No {{MODEL_DISPLAY_NAME}}s yet. Add your first one!'}
</div>
) : (
<div className="record-list">
{filteredRecords.map(record => (
<div key={record.id} className="record-card">
<div className="record-content">
<h3>{record.x_name}</h3>
<p className="record-meta">ID: {record.id}</p>
</div>
<div className="record-actions">
<button
className="delete-btn"
onClick={() => handleDelete(record.id)}
>
🗑️
</button>
</div>
</div>
))}
</div>
)}
</div>
);
}
export default ListPage;

View File

@@ -0,0 +1,12 @@
# Odoo Instance Configuration
ODOO_URL=https://your-instance.odoo.com
ODOO_DB=your-database-name
ODOO_USERNAME=your-username
ODOO_API_KEY=your-api-key
# Primary Model (use x_ prefix for Odoo Studio models)
ODOO_PRIMARY_MODEL=x_{{MODEL_NAME}}
# Optional: For static hosting (GitHub Pages, Cloudflare Pages, etc.)
# Set this to the full URL of your API server if frontend and backend are on different domains
PUBLIC_API_URL=

View File

@@ -0,0 +1,15 @@
node_modules/
.env
.env.local
dist/
build/
.svelte-kit/
.vercel/
.netlify/
.DS_Store
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

View File

@@ -0,0 +1,14 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": false,
"moduleResolution": "bundler"
}
}

View File

@@ -0,0 +1,27 @@
{
"name": "{{PROJECT_NAME}}",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^6.1.0",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.43.2",
"@sveltejs/vite-plugin-svelte": "^6.2.0",
"@vite-pwa/sveltekit": "^1.0.1",
"svelte": "^5.39.5",
"svelte-check": "^4.3.2",
"typescript": "^5.9.2",
"vite": "^7.1.7"
},
"dependencies": {
"vite-plugin-pwa": "^1.1.0"
}
}

View File

@@ -0,0 +1,23 @@
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: undefined,
precompress: false,
strict: true
}),
// Configure base path for GitHub Pages deployment
paths: {
base: process.argv.includes('dev') ? '' : process.env.PUBLIC_BASE_PATH || ''
}
}
};
export default config;

View File

@@ -0,0 +1,40 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [
sveltekit(),
VitePWA({
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,json,webp}']
},
manifest: {
id: '/',
name: '{{PROJECT_NAME}}',
short_name: '{{PROJECT_NAME}}',
description: 'PWA for {{MODEL_DISPLAY_NAME}} management with Odoo integration',
start_url: '/',
scope: '/',
display: 'standalone',
background_color: '#ffffff',
theme_color: '#667eea',
icons: [
{
src: '/icon-192.png',
sizes: '192x192',
type: 'image/png',
purpose: 'any maskable'
},
{
src: '/icon-512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any maskable'
}
]
}
})
]
});

View File

@@ -0,0 +1,48 @@
name: Deploy to GitHub Pages
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- run: npm ci
- run: npm run build
env:
PUBLIC_BASE_PATH: /${{ github.event.repository.name }}
ODOO_URL: ${{ secrets.ODOO_URL }}
ODOO_DB: ${{ secrets.ODOO_DB }}
ODOO_USERNAME: ${{ secrets.ODOO_USERNAME }}
ODOO_API_KEY: ${{ secrets.ODOO_API_KEY }}
ODOO_PRIMARY_MODEL: ${{ secrets.ODOO_PRIMARY_MODEL }}
- uses: actions/upload-pages-artifact@v2
with:
path: build
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2

View File

@@ -0,0 +1,6 @@
{
"buildCommand": "npm run build",
"outputDirectory": "build",
"framework": "sveltekit",
"regions": ["iad1"]
}

View File

@@ -0,0 +1,243 @@
# CLAUDE.md
This file provides guidance to Claude Code when working with this {{PROJECT_NAME}} codebase.
## Project Overview
**{{PROJECT_NAME}}** - An offline-first Progressive Web App for {{MODEL_DISPLAY_NAME}} management, built with SvelteKit frontend and Odoo Studio backend.
## Development Commands
### Development
```bash
npm run dev # Start dev server at http://localhost:5173
```
### Building
```bash
npm run build # Production build (outputs to /build)
npm run preview # Preview production build locally
```
### Type Checking
```bash
npm run check # Run svelte-check for type errors
npm run check:watch # Watch mode for type checking
```
## Environment Setup
Required environment variables in `.env`:
```env
ODOO_URL=https://your-instance.odoo.com
ODOO_DB=your-database-name
ODOO_USERNAME=your-username
ODOO_API_KEY=your-api-key
ODOO_PRIMARY_MODEL=x_{{MODEL_NAME}}
```
**Important**: The app uses API keys (not passwords) for Odoo authentication. These are server-side only and never exposed to the client.
## Architecture
### Three-Layer Data Flow
```
Frontend Component (Svelte)
Cache Store (Svelte Store)
Odoo API Client (src/lib/odoo.js)
Server Route (src/routes/api/odoo/+server.js)
Odoo JSON-RPC Backend
```
Data also persists to localStorage for offline access.
### Key Architectural Patterns
#### 1. **Smart Caching Layer** (`src/lib/stores/cache.js`)
The centerpiece of the frontend architecture. This Svelte store provides:
- **Immediate data availability**: Shows cached data from localStorage instantly on page load
- **Background sync**: Automatically syncs with Odoo every 3 minutes if cache is stale (>5 minutes old)
- **Incremental fetching**: Only fetches records with `id > lastRecordId` to minimize API calls
- **Optimistic updates**: UI updates immediately, syncs to server in background
**Key functions**:
- `initialize()` - Call in `onMount()`, loads cache and starts background sync
- `sync()` - Incremental sync with Odoo
- `forceRefresh()` - Clears cache and does full sync
- `createRecord()`, `updateRecord()`, `deleteRecord()` - CRUD operations with optimistic updates
#### 2. **Server-Side API Proxy** (`src/routes/api/odoo/+server.js`)
SvelteKit server route that acts as a JSON-RPC proxy to Odoo. This pattern:
- Keeps credentials server-side (never exposed to client)
- Caches Odoo UID to reduce authentication calls
- Provides a simple `{ action, data }` interface for the frontend
**Supported actions**: `create`, `search`, `search_model`, `update`, `delete`
#### 3. **Odoo Field Formatting**
Odoo uses specific formats for relational fields:
- **Many2One (single relation)**: Send as integer ID, receive as `[id, "display_name"]` tuple
- **Many2Many (multiple relations)**: Send as `[[6, 0, [id1, id2, ...]]]`, receive as array of tuples
Helper functions in `src/lib/odoo.js`:
- `formatMany2one(id)` - Converts ID to integer or `false`
- `formatMany2many(ids)` - Wraps IDs in Odoo command format `[[6, 0, [...]]]`
#### 4. **Offline-First Strategy**
Two-phase data loading:
1. **Immediate Load**: Show cached data from localStorage
2. **Background Sync**: Fetch new data if cache is stale
```javascript
// Phase 1: Load from cache immediately
const cachedData = loadFromStorage();
updateUI(cachedData);
// Phase 2: Background sync if stale
if (cachedData.isStale) {
syncInBackground();
}
```
### SvelteKit Configuration
- **Rendering**: `ssr: false`, `csr: true`, `prerender: true` in `src/routes/+layout.js`
- **Adapter**: `adapter-static` configured for static output to `/build` directory
- **Base path**: Configurable via `PUBLIC_BASE_PATH` env var (for GitHub Pages deployment)
### PWA Features
Configured in `vite.config.js`:
- **Service Worker**: Auto-generated with Workbox, caches all static assets
- **Auto-update**: New versions automatically activate
- **Manifest**: Configured for standalone mode, installable on mobile
- **Icons**: 192x192 and 512x512 PNG icons in `/static`
## Odoo Model Structure
### Main Model: `x_{{MODEL_NAME}}`
| Field | Type | Purpose |
|-------|------|---------|
| `x_name` | Char | Record name |
| Add your custom fields with x_studio_ prefix |
## Important Development Notes
### Working with the Cache
When modifying record-related features:
1. **Always call `{{MODEL_NAME}}Cache.sync()` after mutations** (create/update/delete)
2. **Initialize the cache in page components**: `onMount(() => {{MODEL_NAME}}Cache.initialize())`
3. **Clean up on unmount**: `onDestroy(() => {{MODEL_NAME}}Cache.destroy())`
4. **Cache invalidation**: Increment cache version in `cache.js` if store schema changes
### Odoo API Patterns
When adding new Odoo operations:
1. **Frontend**: Add method to `src/lib/odoo.js` (calls `/api/odoo` endpoint)
2. **Backend**: Add new action case in `src/routes/api/odoo/+server.js`
3. **Use `execute()` helper** for model operations (wraps authentication)
Example:
```javascript
// Frontend (odoo.js)
async customOperation(id) {
return this.callApi('custom', { id });
}
// Backend (+server.js)
case 'custom':
const result = await execute('x_{{MODEL_NAME}}', 'write', [[data.id], { custom_field: true }]);
return json({ success: true, result });
```
### PWA Manifest Updates
When changing app name, icons, or theme:
1. Update `vite.config.js` manifest section
2. Update `/static/manifest.json`
3. Replace `/static/icon-192.png` and `/static/icon-512.png`
4. Run `npm run build` to regenerate service worker
### Deployment
**Vercel** (primary):
- Automatically deploys from `main` branch
- Set environment variables in Vercel dashboard
- Outputs to `/build` directory (configured in `vercel.json`)
**GitHub Pages**:
- Set `PUBLIC_BASE_PATH=/repo-name` in GitHub Actions secrets
- Configure GitHub Pages source as "GitHub Actions"
- Workflow auto-deploys on push to `main`
## File Structure Reference
```
src/
├── lib/
│ ├── stores/
│ │ └── cache.js # Core caching & sync logic
│ ├── odoo.js # Frontend API client
│ ├── db.js # IndexedDB manager
│ └── utils.js # Utility functions
├── routes/
│ ├── +layout.js # Root layout config (SSR/CSR settings)
│ ├── +layout.svelte # Root layout component
│ ├── +page.svelte # Add record form
│ ├── list/+page.svelte # List all records
│ └── api/odoo/+server.js # Odoo JSON-RPC proxy endpoint
└── app.html # HTML template with PWA meta tags
```
## Common Gotchas
1. **Odoo field naming**: All custom fields use `x_studio_` prefix (Odoo Studio convention)
2. **Partner name format**: Odoo returns `[id, "name"]` tuples, must extract display name for UI
3. **localStorage limits**: Browser typically allows 5-10MB, sufficient for hundreds of records
4. **Service worker caching**: After PWA updates, users may need to close all tabs and reopen
5. **Base path in production**: GitHub Pages deployments require `PUBLIC_BASE_PATH` env var set
6. **API authentication**: Use API keys, not passwords. Generate in Odoo user settings.
## Adding New Features
### Add a New Field
1. Add field in Odoo Studio with `x_studio_` prefix
2. Update `fields` array in `src/lib/stores/cache.js`
3. Add form input in `src/routes/+page.svelte`
4. Display field in `src/routes/list/+page.svelte`
### Add a New Page
1. Create `src/routes/new-page/+page.svelte`
2. Add navigation link in layout or other pages
3. Import and use cache store if accessing data
### Add Partner/Relation Field
1. Add Many2one or Many2many field in Odoo
2. Use `odooClient.formatMany2one()` or `formatMany2many()` when saving
3. Use `odooClient.fetchPartners()` to load options
4. Display resolved names in UI
---
**Generated with Odoo PWA Generator**

View File

@@ -0,0 +1,225 @@
# {{PROJECT_NAME}}
An offline-first Progressive Web App for {{MODEL_DISPLAY_NAME}} management with Odoo Studio backend integration.
## ✨ Features
- 📱 **Progressive Web App** - Installable on mobile and desktop
- 🔌 **Offline-first** - Works without internet connection
- 🔄 **Auto-sync** - Background synchronization with Odoo
- 💾 **Smart caching** - localStorage + IndexedDB for optimal performance
- 🎨 **Responsive UI** - Works on all devices
- ⚡ **Fast** - Instant UI updates with optimistic rendering
## 🚀 Quick Start
### Prerequisites
- Node.js 18+ installed
- Odoo Studio account with API access
- Custom Odoo model created: `x_{{MODEL_NAME}}`
### Installation
1. Clone this repository:
```bash
git clone <repository-url>
cd {{PROJECT_NAME}}
```
2. Install dependencies:
```bash
npm install
```
3. Configure environment:
```bash
cp .env.example .env
```
4. Edit `.env` with your Odoo credentials:
```env
ODOO_URL=https://your-instance.odoo.com
ODOO_DB=your-database-name
ODOO_USERNAME=your-username
ODOO_API_KEY=your-api-key
ODOO_PRIMARY_MODEL=x_{{MODEL_NAME}}
```
5. Start development server:
```bash
npm run dev
```
6. Open [http://localhost:5173](http://localhost:5173)
## 🔧 Odoo Studio Setup
### Create Custom Model
1. Log into your Odoo instance
2. Navigate to **Studio**
3. Create a new model: `x_{{MODEL_NAME}}`
4. Add these recommended fields:
- `x_name` (Char) - Required - Record name
- `x_studio_description` (Text) - Description
- `x_studio_date` (Date) - Date field
- `x_studio_value` (Float) - Numeric value
- Add more custom fields as needed with `x_studio_` prefix
### Get API Credentials
1. Go to **Settings** → **Users & Companies** → **Users**
2. Select your user
3. Click **Generate API Key**
4. Copy the key and add to `.env` file
## 📦 Build & Deployment
### Build for Production
```bash
npm run build
```
### Deploy to Vercel
```bash
npm install -g vercel
vercel
```
Set environment variables in Vercel dashboard:
- `ODOO_URL`
- `ODOO_DB`
- `ODOO_USERNAME`
- `ODOO_API_KEY`
- `ODOO_PRIMARY_MODEL`
### Deploy to GitHub Pages
1. Add secrets to GitHub repository:
- `ODOO_URL`, `ODOO_DB`, `ODOO_USERNAME`, `ODOO_API_KEY`, `ODOO_PRIMARY_MODEL`
2. Enable GitHub Pages in repository settings:
- Source: GitHub Actions
3. Push to main branch - auto-deploys via GitHub Actions
### Deploy to Cloudflare Pages
```bash
npm run build
wrangler pages publish build
```
## 🏗️ Architecture
This app follows a three-layer architecture:
```
Frontend Component
Cache Store (Svelte Store)
Odoo API Client (Frontend)
Server Route (/api/odoo)
Odoo JSON-RPC
```
### Key Features:
- **Smart Caching**: Loads from cache instantly, syncs in background
- **Incremental Sync**: Only fetches new records since last sync
- **Offline Queue**: Saves changes locally when offline, syncs when online
- **PWA**: Service worker caches all assets for offline use
See `CLAUDE.md` for detailed architecture documentation.
## 🛠️ Development
### Available Scripts
- `npm run dev` - Start development server
- `npm run build` - Build for production
- `npm run preview` - Preview production build
- `npm run check` - Type check
### Project Structure
```
{{PROJECT_NAME}}/
├── src/
│ ├── lib/
│ │ ├── odoo.js # Odoo API client
│ │ ├── db.js # IndexedDB manager
│ │ ├── utils.js # Utility functions
│ │ └── stores/
│ │ └── cache.js # Smart cache store
│ ├── routes/
│ │ ├── +layout.svelte # Root layout
│ │ ├── +page.svelte # Add record form
│ │ ├── list/+page.svelte # List all records
│ │ └── api/odoo/+server.js # Server API proxy
│ └── app.html # HTML template
├── static/
│ ├── manifest.json # PWA manifest
│ ├── icon-192.png # App icon 192x192
│ └── icon-512.png # App icon 512x512
├── .env # Environment variables (gitignored)
├── .env.example # Environment template
├── svelte.config.js # SvelteKit configuration
├── vite.config.js # Vite + PWA configuration
└── package.json # Dependencies
```
## 📝 Adding Fields
To add custom fields to your model:
1. Add field in Odoo Studio (use `x_studio_` prefix)
2. Update `src/lib/stores/cache.js` - add field to `fields` array
3. Update `src/routes/+page.svelte` - add form input
4. Update `src/routes/list/+page.svelte` - display field
## 🐛 Troubleshooting
### "Authentication failed"
- Verify `ODOO_API_KEY` is correct
- Check `ODOO_USERNAME` matches your Odoo user
- Ensure API access is enabled in Odoo
### "Model not found"
- Verify model name uses `x_` prefix: `x_{{MODEL_NAME}}`
- Check model exists in Odoo Studio
- Ensure model is published
### Offline mode not working
- Check service worker is registered (DevTools → Application)
- Verify PWA manifest is loaded
- Clear browser cache and reload
### Data not syncing
- Check browser console for errors
- Verify internet connection
- Check Odoo API is accessible
## 📚 Learn More
- [SvelteKit Documentation](https://kit.svelte.dev/)
- [Odoo API Documentation](https://www.odoo.com/documentation/)
- [PWA Guide](https://web.dev/progressive-web-apps/)
## 📄 License
MIT
## 🤝 Contributing
Contributions welcome! Please open an issue or PR.
---
**Generated with [Odoo PWA Generator](https://github.com/jamshid/odoo-pwa-generator)** 🚀

View File

@@ -0,0 +1,134 @@
import { json } from '@sveltejs/kit';
import {
ODOO_URL,
ODOO_DB,
ODOO_USERNAME,
ODOO_API_KEY
} from '$env/static/private';
/** @type {number|null} */
let cachedUid = null;
/**
* Make JSON-RPC call to Odoo
* @param {string} service
* @param {string} method
* @param {any[]} args
*/
async function callOdoo(service, method, args) {
const response = await fetch(`${ODOO_URL}/jsonrpc`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'call',
params: {
service: service,
method: method,
args: args
},
id: Math.floor(Math.random() * 1000000)
})
});
const data = await response.json();
if (data.error) {
throw new Error(data.error.data?.message || data.error.message || 'Odoo API Error');
}
return data.result;
}
/**
* Authenticate with Odoo and get UID
*/
async function authenticate() {
if (cachedUid) return cachedUid;
const authMethod = ODOO_API_KEY;
const uid = await callOdoo('common', 'login', [ODOO_DB, ODOO_USERNAME, authMethod]);
if (!uid) {
throw new Error('Authentication failed');
}
cachedUid = uid;
return uid;
}
/**
* Execute a method on Odoo model
* @param {string} model
* @param {string} method
* @param {any[]} args
* @param {Record<string, any>} kwargs
*/
async function execute(model, method, args = [], kwargs = {}) {
const uid = await authenticate();
const authMethod = ODOO_API_KEY;
return await callOdoo('object', 'execute_kw', [
ODOO_DB,
uid,
authMethod,
model,
method,
args,
kwargs
]);
}
/** @type {import('./$types').RequestHandler} */
export async function POST({ request }) {
try {
const { action, data } = await request.json();
switch (action) {
case 'create': {
const { model, fields } = data;
const id = await execute(model, 'create', [fields]);
return json({ success: true, id });
}
case 'search': {
const { model, domain = [], fields = [] } = data;
const results = await execute(model, 'search_read', [domain], { fields });
return json({ success: true, results });
}
// Search any model (used by frontend to load res.partner list, etc.)
case 'search_model': {
const { model, domain = [], fields = [] } = data;
const results = await execute(model, 'search_read', [domain], { fields });
return json({ success: true, results });
}
case 'update': {
const { model, id, values } = data;
const result = await execute(model, 'write', [[id], values]);
return json({ success: true, result });
}
case 'delete': {
const { model, id } = data;
const result = await execute(model, 'unlink', [[id]]);
return json({ success: true, result });
}
default:
return json({ success: false, error: 'Invalid action' }, { status: 400 });
}
} catch (error) {
console.error('Odoo API Error:', error);
return json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,311 @@
<script>
import { {{MODEL_NAME}}Cache, cacheStatus } from '$lib/stores/cache';
import { filterRecords } from '$lib/utils';
import { onMount, onDestroy } from 'svelte';
let searchTerm = $state('');
let records = $derived(${{MODEL_NAME}}Cache.records);
let filteredRecords = $derived(filterRecords(records, searchTerm, ['x_name']));
let status = $derived($cacheStatus);
onMount(async () => {
await {{MODEL_NAME}}Cache.initialize();
});
onDestroy(() => {
{{MODEL_NAME}}Cache.destroy();
});
async function handleDelete(id) {
if (!confirm('Are you sure you want to delete this {{MODEL_DISPLAY_NAME}}?')) {
return;
}
try {
await {{MODEL_NAME}}Cache.deleteRecord(id);
} catch (error) {
alert(`Failed to delete: ${error.message}`);
}
}
function handleRefresh() {
{{MODEL_NAME}}Cache.forceRefresh();
}
</script>
<svelte:head>
<title>All {{MODEL_DISPLAY_NAME}}s - {{PROJECT_NAME}}</title>
</svelte:head>
<div class="container">
<h1>📋 {{PROJECT_NAME}}</h1>
<nav>
<a href="/">Add {{MODEL_DISPLAY_NAME}}</a>
<a href="/list" class="active">View All</a>
</nav>
<div class="list-container">
<div class="list-header">
<h2>All {{MODEL_DISPLAY_NAME}}s</h2>
<div class="header-actions">
<input
type="search"
placeholder="Search..."
bind:value={searchTerm}
class="search-input"
/>
<button class="refresh-btn" onclick={handleRefresh} disabled={status.isSyncing}>
{status.isSyncing ? '⏳' : '🔄'}
</button>
</div>
</div>
{#if status.isLoading}
<div class="loading">Loading...</div>
{:else if filteredRecords.length === 0}
<div class="empty">
{searchTerm ? 'No matching records found' : 'No {{MODEL_DISPLAY_NAME}}s yet. Add your first one!'}
</div>
{:else}
<div class="record-list">
{#each filteredRecords as record (record.id)}
<div class="record-card">
<div class="record-content">
<h3>{record.x_name}</h3>
<!-- Add more fields to display -->
<p class="record-meta">ID: {record.id}</p>
</div>
<div class="record-actions">
<button class="delete-btn" onclick={() => handleDelete(record.id)}>
🗑️
</button>
</div>
</div>
{/each}
</div>
{/if}
{#if status.lastSync > 0}
<div class="sync-info">
Last synced: {new Date(status.lastSync).toLocaleString()}
{#if status.isStale}
<span class="stale-badge">Stale</span>
{/if}
</div>
{/if}
</div>
</div>
<style>
.container {
max-width: 800px;
margin: 0 auto;
padding: 16px;
}
h1 {
color: white;
text-align: center;
margin-bottom: 30px;
font-size: 2.5em;
}
h2 {
margin: 0;
color: #333;
font-size: 1.5em;
}
nav {
display: flex;
gap: 10px;
margin-bottom: 30px;
background: white;
border-radius: 10px;
padding: 5px;
}
nav a {
flex: 1;
text-align: center;
padding: 12px;
text-decoration: none;
color: #667eea;
border-radius: 8px;
font-weight: 600;
transition: all 0.3s;
}
nav a.active {
background: #667eea;
color: white;
}
.list-container {
background: white;
padding: 24px;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 15px;
}
.header-actions {
display: flex;
gap: 10px;
align-items: center;
}
.search-input {
padding: 10px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
min-width: 200px;
}
.search-input:focus {
outline: none;
border-color: #667eea;
}
.refresh-btn {
padding: 10px 15px;
background: #f0f0f0;
color: #667eea;
border: none;
border-radius: 8px;
font-size: 18px;
cursor: pointer;
transition: all 0.3s;
}
.refresh-btn:hover:not(:disabled) {
background: #e0e0e0;
transform: rotate(360deg);
}
.refresh-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.loading,
.empty {
text-align: center;
padding: 40px;
color: #666;
font-size: 16px;
}
.record-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.record-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border: 2px solid #e0e0e0;
border-radius: 10px;
transition: all 0.3s;
}
.record-card:hover {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1);
}
.record-content {
flex: 1;
}
.record-content h3 {
margin: 0 0 8px 0;
color: #333;
font-size: 1.1em;
}
.record-meta {
margin: 0;
color: #666;
font-size: 0.9em;
}
.record-actions {
display: flex;
gap: 8px;
}
.delete-btn {
padding: 8px 12px;
background: #fff5f5;
color: #e53e3e;
border: 1px solid #feb2b2;
border-radius: 6px;
font-size: 18px;
cursor: pointer;
transition: all 0.3s;
}
.delete-btn:hover {
background: #fed7d7;
}
.sync-info {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #e0e0e0;
text-align: center;
color: #666;
font-size: 0.9em;
}
.stale-badge {
display: inline-block;
margin-left: 10px;
padding: 2px 8px;
background: #fef5e7;
color: #f39c12;
border-radius: 4px;
font-size: 0.85em;
font-weight: 600;
}
@media (max-width: 600px) {
.list-header {
flex-direction: column;
align-items: stretch;
}
.header-actions {
flex-direction: column;
align-items: stretch;
}
.search-input {
min-width: auto;
}
.record-card {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.record-actions {
align-self: flex-end;
}
}
</style>

View File

@@ -0,0 +1,4 @@
// Disable SSR for this app (client-side only with Odoo backend)
export const ssr = false;
export const csr = true;
export const prerender = true;

View File

@@ -0,0 +1,24 @@
<script>
let { children } = $props();
</script>
<svelte:head>
<title>{{PROJECT_NAME}} - {{MODEL_DISPLAY_NAME}} Manager</title>
<meta name="description" content="Offline-first {{MODEL_DISPLAY_NAME}} management app with Odoo integration" />
</svelte:head>
{@render children?.()}
<style>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu,
Cantarell, 'Helvetica Neue', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
:global(*) {
box-sizing: border-box;
}
</style>

View File

@@ -0,0 +1,271 @@
<script>
import { {{MODEL_NAME}}Cache } from '$lib/stores/cache';
import { onMount, onDestroy } from 'svelte';
let name = $state('');
let loading = $state(false);
let message = $state('');
let isOffline = $state(!navigator.onLine);
// Listen for online/offline events
if (typeof window !== 'undefined') {
window.addEventListener('online', () => { isOffline = false; });
window.addEventListener('offline', () => { isOffline = true; });
}
onMount(async () => {
await {{MODEL_NAME}}Cache.initialize();
});
onDestroy(() => {
{{MODEL_NAME}}Cache.destroy();
});
async function handleSubmit() {
if (!name.trim()) {
message = '⚠️ Please fill in the name field';
return;
}
loading = true;
message = '';
try {
const payload = {
x_name: name
// Add more fields as needed
};
await {{MODEL_NAME}}Cache.createRecord(payload);
if (navigator.onLine) {
message = '✅ {{MODEL_DISPLAY_NAME}} added successfully!';
} else {
message = '✅ {{MODEL_DISPLAY_NAME}} saved locally! Will sync when online.';
}
// Reset form
name = '';
} catch (error) {
message = `❌ Error: ${error.message}`;
} finally {
loading = false;
}
}
function handleRefresh() {
{{MODEL_NAME}}Cache.forceRefresh();
}
</script>
<svelte:head>
<title>Add {{MODEL_DISPLAY_NAME}} - {{PROJECT_NAME}}</title>
</svelte:head>
<div class="container">
<h1>📋 {{PROJECT_NAME}}</h1>
<!-- Offline Indicator -->
{#if isOffline}
<div class="offline-banner">
📡 Offline Mode - Data will be synced when you're back online
</div>
{/if}
<nav>
<a href="/" class="active">Add {{MODEL_DISPLAY_NAME}}</a>
<a href="/list">View All</a>
</nav>
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<div class="form-group">
<label for="name">Name</label>
<input
type="text"
id="name"
bind:value={name}
placeholder="Enter {{MODEL_DISPLAY_NAME}} name"
required
/>
</div>
<!-- Add more form fields based on your Odoo model -->
{#if message}
<div class="message" class:error={message.includes('❌')}>{message}</div>
{/if}
<div class="button-group">
<button type="submit" disabled={loading}>
{loading ? '⏳ Adding...' : ' Add {{MODEL_DISPLAY_NAME}}'}
</button>
{#if !isOffline}
<button type="button" class="refresh-btn" onclick={handleRefresh}>
🔄 Refresh Data
</button>
{/if}
</div>
</form>
</div>
<style>
.container {
max-width: 600px;
margin: 0 auto;
padding: 16px;
}
@media (max-width: 480px) {
.container {
padding: 12px;
}
}
h1 {
color: white;
text-align: center;
margin-bottom: 30px;
font-size: 2.5em;
}
nav {
display: flex;
gap: 10px;
margin-bottom: 30px;
background: white;
border-radius: 10px;
padding: 5px;
}
nav a {
flex: 1;
text-align: center;
padding: 12px;
text-decoration: none;
color: #667eea;
border-radius: 8px;
font-weight: 600;
transition: all 0.3s;
}
nav a.active {
background: #667eea;
color: white;
}
.offline-banner {
background: #e3f2fd;
color: #1565c0;
padding: 12px 20px;
border-radius: 10px;
margin-bottom: 20px;
text-align: center;
font-weight: 600;
border: 2px solid #64b5f6;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; }
}
form {
background: white;
padding: 24px;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
}
@media (max-width: 480px) {
form {
padding: 16px;
border-radius: 12px;
}
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
}
input,
select,
textarea {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: #667eea;
}
.button-group {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
flex: 1;
min-width: 150px;
padding: 15px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
button:hover:not(:disabled) {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.refresh-btn {
background: #f0f0f0;
color: #667eea;
flex: 0 1 auto;
}
.refresh-btn:hover:not(:disabled) {
background: #e0e0e0;
color: #5568d3;
}
.message {
padding: 12px;
border-radius: 8px;
margin-bottom: 15px;
background: #d4edda;
color: #155724;
text-align: center;
}
.message.error {
background: #f8d7da;
color: #721c24;
}
</style>

View File

@@ -0,0 +1,24 @@
{
"name": "{{PROJECT_NAME}}",
"short_name": "{{PROJECT_NAME}}",
"description": "{{MODEL_DISPLAY_NAME}} management app with Odoo integration",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#667eea",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}

View File

@@ -0,0 +1,11 @@
# Odoo Instance Configuration (for backend server)
ODOO_URL=https://your-instance.odoo.com
ODOO_DB=your-database-name
ODOO_USERNAME=your-username
ODOO_API_KEY=your-api-key
# Primary Model (use x_ prefix for Odoo Studio models)
ODOO_PRIMARY_MODEL=x_{{MODEL_NAME}}
# Frontend: API endpoint (if frontend and backend are separate)
VITE_API_URL=http://localhost:3000

View File

@@ -0,0 +1,35 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
# Environment
.env
.env.local
.env.production

View File

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#667eea" />
<meta name="description" content="{{MODEL_DISPLAY_NAME}} management PWA with Odoo integration" />
<link rel="manifest" href="/manifest.json" />
<title>{{PROJECT_NAME}}</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,20 @@
{
"name": "{{PROJECT_NAME}}",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.4.29",
"vue-router": "^4.3.3"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.5",
"vite": "^5.3.1",
"vite-plugin-pwa": "^0.20.0"
}
}

View File

@@ -0,0 +1,49 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { VitePWA } from 'vite-plugin-pwa';
import { fileURLToPath, URL } from 'node:url';
export default defineConfig({
plugins: [
vue(),
VitePWA({
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,json,webp}']
},
manifest: {
name: '{{PROJECT_NAME}}',
short_name: '{{PROJECT_NAME}}',
description: 'PWA for {{MODEL_DISPLAY_NAME}} management with Odoo integration',
theme_color: '#667eea',
background_color: '#ffffff',
display: 'standalone',
icons: [
{
src: '/icon-192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: '/icon-512.png',
sizes: '512x512',
type: 'image/png'
}
]
}
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
proxy: {
'/api': {
target: process.env.VITE_API_URL || 'http://localhost:3000',
changeOrigin: true
}
}
}
});

View File

@@ -0,0 +1,74 @@
<template>
<div class="app">
<div class="container">
<h1>📋 {{PROJECT_NAME}}</h1>
<OfflineBanner />
<nav class="nav">
<router-link
to="/"
:class="{ active: $route.path === '/' }"
>
Add {{MODEL_DISPLAY_NAME}}
</router-link>
<router-link
to="/list"
:class="{ active: $route.path === '/list' }"
>
View All
</router-link>
</nav>
<router-view />
</div>
</div>
</template>
<script setup>
import OfflineBanner from './components/OfflineBanner.vue';
</script>
<style scoped>
.app {
min-height: 100vh;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 16px;
}
h1 {
color: white;
text-align: center;
margin-bottom: 30px;
font-size: 2.5em;
}
.nav {
display: flex;
gap: 10px;
margin-bottom: 30px;
background: white;
border-radius: 10px;
padding: 5px;
}
.nav a {
flex: 1;
text-align: center;
padding: 12px;
text-decoration: none;
color: #667eea;
border-radius: 8px;
font-weight: 600;
transition: all 0.3s;
}
.nav a.active {
background: #667eea;
color: white;
}
</style>

View File

@@ -0,0 +1,123 @@
/**
* Odoo API Client for React
* Communicates with backend server that proxies to Odoo
*/
class OdooAPI {
constructor() {
this.apiUrl = import.meta.env.VITE_API_URL
? `${import.meta.env.VITE_API_URL}/api/odoo`
: '/api/odoo';
}
/**
* Call the server-side API
* @param {string} action
* @param {any} data
* @returns {Promise<any>}
*/
async callApi(action, data) {
const response = await fetch(this.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ action, data })
});
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'API Error');
}
return result;
}
/**
* Create a new record in specified model
* @param {string} model - Odoo model name
* @param {Record<string, any>} fields - Record fields
* @returns {Promise<number>} - Created record ID
*/
async createRecord(model, fields) {
const result = await this.callApi('create', { model, fields });
return result.id;
}
/**
* Search and read records from specified model
* @param {string} model - Odoo model name
* @param {any[]} domain - Odoo domain filter
* @param {string[]} fields - Fields to retrieve
* @returns {Promise<any[]>} - Array of records
*/
async searchRecords(model, domain = [], fields = []) {
const result = await this.callApi('search', { model, domain, fields });
return result.results;
}
/**
* Generic search_read for any model
* @param {string} model
* @param {any[]} domain
* @param {string[]} fields
* @returns {Promise<any[]>}
*/
async searchModel(model, domain = [], fields = []) {
const result = await this.callApi('search_model', { model, domain, fields });
return result.results;
}
/**
* Fetch partner list (common operation)
* @returns {Promise<Array<{id:number, display_name:string}>>}
*/
async fetchPartners() {
return await this.searchModel('res.partner', [], ['id', 'display_name']);
}
/**
* Format a many2one field value
* @param {number|string|null|undefined} id
* @returns {number|false}
*/
formatMany2one(id) {
return id ? Number(id) : false;
}
/**
* Format a many2many field using the (6,0,[ids]) command
* @param {Array<number|string>} ids
* @returns {any[]}
*/
formatMany2many(ids) {
if (!Array.isArray(ids) || ids.length === 0) return [];
return [[6, 0, ids.map((i) => Number(i))]];
}
/**
* Update a record
* @param {string} model - Odoo model name
* @param {number} id - Record ID
* @param {Record<string, any>} values - Fields to update
* @returns {Promise<boolean>}
*/
async updateRecord(model, id, values) {
const result = await this.callApi('update', { model, id, values });
return result.result;
}
/**
* Delete a record
* @param {string} model - Odoo model name
* @param {number} id - Record ID
* @returns {Promise<boolean>}
*/
async deleteRecord(model, id) {
const result = await this.callApi('delete', { model, id });
return result.result;
}
}
export const odooClient = new OdooAPI();

View File

@@ -0,0 +1,48 @@
<template>
<div v-if="isOffline" class="offline-banner">
📡 Offline Mode - Data will be synced when you're back online
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const isOffline = ref(!navigator.onLine);
const handleOnline = () => isOffline.value = false;
const handleOffline = () => isOffline.value = true;
onMounted(() => {
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
});
onUnmounted(() => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
});
</script>
<style scoped>
.offline-banner {
background: #e3f2fd;
color: #1565c0;
padding: 12px 20px;
border-radius: 10px;
margin-bottom: 20px;
text-align: center;
font-weight: 600;
border: 2px solid #64b5f6;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.8;
}
}
</style>

View File

@@ -0,0 +1,224 @@
import { ref, onMounted, onUnmounted } from 'vue';
import { odooClient } from '../api/odoo';
// Cache configuration
const CACHE_KEY = '{{MODEL_NAME}}_cache_v1';
const CACHE_META_KEY = '{{MODEL_NAME}}_cache_meta_v1';
const CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes
const SYNC_INTERVAL_MS = 3 * 60 * 1000; // 3 minutes
// Helper functions for localStorage
function loadFromStorage() {
try {
const cachedData = localStorage.getItem(CACHE_KEY);
const meta = localStorage.getItem(CACHE_META_KEY);
if (cachedData && meta) {
const records = JSON.parse(cachedData);
const metaData = JSON.parse(meta);
const now = Date.now();
const isStale = now - metaData.lastSyncTime > CACHE_DURATION_MS;
return { records, meta: { ...metaData, isStale } };
}
} catch (e) {
console.warn('Failed to load cache:', e);
}
return {
records: [],
meta: {
lastSyncTime: 0,
lastRecordId: 0,
recordCount: 0,
isStale: true
}
};
}
function saveToStorage(records, meta) {
try {
localStorage.setItem(CACHE_KEY, JSON.stringify(records));
localStorage.setItem(CACHE_META_KEY, JSON.stringify({
lastSyncTime: meta.lastSyncTime || Date.now(),
lastRecordId: meta.lastRecordId || 0,
recordCount: records.length,
isStale: false
}));
} catch (e) {
console.warn('Failed to save cache:', e);
}
}
export function useTaskCache() {
const records = ref([]);
const loading = ref(false);
const syncing = ref(false);
const error = ref('');
const meta = ref({
lastSyncTime: 0,
lastRecordId: 0,
recordCount: 0,
isStale: true
});
let syncInterval = null;
// Sync function
const sync = async (forceFullRefresh = false) => {
syncing.value = true;
error.value = '';
try {
const fields = ['id', 'x_name'];
let domain = [];
let fetchedRecords = [];
if (!forceFullRefresh && meta.value.lastRecordId > 0) {
domain = [['id', '>', meta.value.lastRecordId]];
try {
fetchedRecords = await odooClient.searchRecords('x_{{MODEL_NAME}}', domain, fields);
} catch (err) {
console.warn('Incremental fetch failed:', err);
forceFullRefresh = true;
}
}
if (forceFullRefresh || meta.value.lastRecordId === 0) {
fetchedRecords = await odooClient.searchRecords('x_{{MODEL_NAME}}', [], fields);
}
let mergedRecords;
if (forceFullRefresh || meta.value.lastRecordId === 0) {
mergedRecords = fetchedRecords;
} else {
const existingIds = new Set(records.value.map(r => r.id));
const newRecords = fetchedRecords.filter(r => !existingIds.has(r.id));
mergedRecords = [...records.value, ...newRecords];
}
mergedRecords.sort((a, b) => a.id - b.id);
const lastRecordId = mergedRecords.length > 0
? Math.max(...mergedRecords.map(r => r.id))
: 0;
const newMeta = {
lastSyncTime: Date.now(),
lastRecordId,
recordCount: mergedRecords.length,
isStale: false
};
saveToStorage(mergedRecords, newMeta);
records.value = mergedRecords;
meta.value = newMeta;
} catch (err) {
console.error('Sync failed:', err);
error.value = err.message || 'Failed to sync data';
} finally {
syncing.value = false;
}
};
// Initialize
const initialize = async () => {
const cachedState = loadFromStorage();
if (cachedState.records.length > 0) {
records.value = cachedState.records;
meta.value = cachedState.meta;
if (cachedState.meta.isStale) {
await sync();
}
} else {
loading.value = true;
await sync(true);
loading.value = false;
}
// Set up periodic sync
syncInterval = setInterval(() => {
sync();
}, SYNC_INTERVAL_MS);
};
// Cleanup
const cleanup = () => {
if (syncInterval) {
clearInterval(syncInterval);
syncInterval = null;
}
};
// Force refresh
const forceRefresh = async () => {
await sync(true);
};
// Create record
const createRecord = async (fields) => {
try {
const id = await odooClient.createRecord('x_{{MODEL_NAME}}', fields);
const newRecord = { id, ...fields };
records.value = [...records.value, newRecord];
await sync();
return id;
} catch (err) {
console.error('Failed to create record:', err);
throw err;
}
};
// Update record
const updateRecord = async (id, values) => {
try {
await odooClient.updateRecord('x_{{MODEL_NAME}}', id, values);
records.value = records.value.map(r =>
r.id === id ? { ...r, ...values } : r
);
await sync();
return true;
} catch (err) {
console.error('Failed to update record:', err);
throw err;
}
};
// Delete record
const deleteRecord = async (id) => {
try {
await odooClient.deleteRecord('x_{{MODEL_NAME}}', id);
records.value = records.value.filter(r => r.id !== id);
return true;
} catch (err) {
console.error('Failed to delete record:', err);
throw err;
}
};
// Auto-initialize and cleanup on component lifecycle
onMounted(() => {
initialize();
});
onUnmounted(() => {
cleanup();
});
return {
records,
loading,
syncing,
error,
meta,
sync,
forceRefresh,
createRecord,
updateRecord,
deleteRecord
};
}

View File

@@ -0,0 +1,18 @@
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue';
import AddPage from './pages/AddPage.vue';
import ListPage from './pages/ListPage.vue';
import './style.css';
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: AddPage },
{ path: '/list', component: ListPage }
]
});
createApp(App)
.use(router)
.mount('#app');

View File

@@ -0,0 +1,161 @@
<template>
<form @submit.prevent="handleSubmit" class="form">
<div class="form-group">
<label for="name">Name</label>
<input
type="text"
id="name"
v-model="name"
placeholder="Enter {{MODEL_DISPLAY_NAME}} name"
required
/>
</div>
<div v-if="message" :class="['message', { error: message.includes('❌') }]">
{{ message }}
</div>
<div class="button-group">
<button type="submit" :disabled="loading">
{{ loading ? '⏳ Adding...' : ' Add {{MODEL_DISPLAY_NAME}}' }}
</button>
<button
v-if="isOnline"
type="button"
class="refresh-btn"
@click="forceRefresh"
>
🔄 Refresh Data
</button>
</div>
</form>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useTaskCache } from '../composables/useTaskCache';
const { createRecord, forceRefresh } = useTaskCache();
const name = ref('');
const loading = ref(false);
const message = ref('');
const isOnline = computed(() => navigator.onLine);
const handleSubmit = async () => {
if (!name.value.trim()) {
message.value = '⚠️ Please fill in the name field';
return;
}
loading.value = true;
message.value = '';
try {
const payload = { x_name: name.value };
await createRecord(payload);
if (navigator.onLine) {
message.value = '✅ {{MODEL_DISPLAY_NAME}} added successfully!';
} else {
message.value = '✅ {{MODEL_DISPLAY_NAME}} saved locally! Will sync when online.';
}
name.value = '';
} catch (error) {
message.value = `❌ Error: ${error.message}`;
} finally {
loading.value = false;
}
};
</script>
<style scoped>
.form {
background: white;
padding: 24px;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
}
input {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
}
input:focus {
outline: none;
border-color: #667eea;
}
.button-group {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
flex: 1;
min-width: 150px;
padding: 15px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
button:hover:not(:disabled) {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.refresh-btn {
background: #f0f0f0;
color: #667eea;
flex: 0 1 auto;
}
.refresh-btn:hover:not(:disabled) {
background: #e0e0e0;
color: #5568d3;
}
.message {
padding: 12px;
border-radius: 8px;
margin-bottom: 15px;
background: #d4edda;
color: #155724;
text-align: center;
}
.message.error {
background: #f8d7da;
color: #721c24;
}
</style>

View File

@@ -0,0 +1,200 @@
<template>
<div class="list-container">
<div class="list-header">
<h2>All {{MODEL_DISPLAY_NAME}}s</h2>
<div class="header-actions">
<input
type="search"
v-model="searchTerm"
placeholder="Search..."
class="search-input"
/>
<button
class="refresh-btn-small"
@click="forceRefresh"
:disabled="syncing"
>
{{ syncing ? '⏳' : '🔄' }}
</button>
</div>
</div>
<div v-if="loading" class="loading">Loading...</div>
<div v-else-if="filteredRecords.length === 0" class="empty">
{{ searchTerm ? 'No matching records found' : 'No {{MODEL_DISPLAY_NAME}}s yet. Add your first one!' }}
</div>
<div v-else class="record-list">
<div
v-for="record in filteredRecords"
:key="record.id"
class="record-card"
>
<div class="record-content">
<h3>{{ record.x_name }}</h3>
<p class="record-meta">ID: {{ record.id }}</p>
</div>
<div class="record-actions">
<button class="delete-btn" @click="handleDelete(record.id)">
🗑️
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useTaskCache } from '../composables/useTaskCache';
const { records, loading, syncing, forceRefresh, deleteRecord } = useTaskCache();
const searchTerm = ref('');
const filteredRecords = computed(() => {
return records.value.filter(record =>
record.x_name?.toLowerCase().includes(searchTerm.value.toLowerCase())
);
});
const handleDelete = async (id) => {
if (!confirm('Are you sure you want to delete this {{MODEL_DISPLAY_NAME}}?')) {
return;
}
try {
await deleteRecord(id);
} catch (error) {
alert(`Failed to delete: ${error.message}`);
}
};
</script>
<style scoped>
.list-container {
background: white;
padding: 24px;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 15px;
}
h2 {
margin: 0;
color: #333;
font-size: 1.5em;
}
.header-actions {
display: flex;
gap: 10px;
align-items: center;
}
.search-input {
padding: 10px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
min-width: 200px;
}
.search-input:focus {
outline: none;
border-color: #667eea;
}
.refresh-btn-small {
padding: 10px 15px;
background: #f0f0f0;
color: #667eea;
border: none;
border-radius: 8px;
font-size: 18px;
cursor: pointer;
transition: all 0.3s;
}
.refresh-btn-small:hover:not(:disabled) {
background: #e0e0e0;
transform: rotate(360deg);
}
.refresh-btn-small:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.loading,
.empty {
text-align: center;
padding: 40px;
color: #666;
font-size: 16px;
}
.record-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.record-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border: 2px solid #e0e0e0;
border-radius: 10px;
transition: all 0.3s;
}
.record-card:hover {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1);
}
.record-content {
flex: 1;
}
.record-content h3 {
margin: 0 0 8px 0;
color: #333;
font-size: 1.1em;
}
.record-meta {
margin: 0;
color: #666;
font-size: 0.9em;
}
.record-actions {
display: flex;
gap: 8px;
}
.delete-btn {
padding: 8px 12px;
background: #fff5f5;
color: #e53e3e;
border: 1px solid #feb2b2;
border-radius: 6px;
font-size: 18px;
cursor: pointer;
transition: all 0.3s;
}
.delete-btn:hover {
background: #fed7d7;
}
</style>

View File

@@ -0,0 +1,29 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu,
Cantarell, 'Helvetica Neue', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
#app {
width: 100%;
min-height: 100vh;
}