docs/MIGRATION.md
Rama main · réplica de lectura del repositorio original.
# Configuration Migration Guide
## Upgrading to the payment rule editor API release (#262)
- **Two new tables, no action needed.** The sync-history database gains `actual_schedule_snapshot` (the Actual schedules captured by each sync) and `payment_alert_rule` (rules created in the dashboard), created on first start. A downgrade ignores them.
- **New routes.** `GET /api/dashboard/payment-schedules/:server`, `GET`, `POST`, `PUT` and `DELETE /api/dashboard/payment-rules/:server[/:alertId]` and `POST /api/dashboard/payment-preview/:server`; the writes need `dashboard.allowConfigWrites: true` and dashboard auth other than `none`. See [HEALTH_CHECK.md](HEALTH_CHECK.md#payment-rules-262).
- **`GET /api/dashboard/capabilities` gains `channels`**, the channels that currently have a destination. A client that compares the whole body must accept the new key.
- **`GET /api/dashboard/payments` `source`** is now `"dashboard"` for a dashboard rule (it was always `"config"`).
- **Empty `alerts` is allowed.** The schema no longer requires `scheduleAlerts.alerts` to have an item, so `"scheduleAlerts": { "alerts": [] }` opts a server in to dashboard rules.
- **Reset semantics.** A history reset (one server or all) deletes the schedule snapshot but keeps dashboard rules, which are configuration. Delete a rule with `DELETE /api/dashboard/payment-rules/:server/:alertId`.
## Upgrading to the alert controls release (#261)
- **New table, no action needed.** The sync-history database gains `payment_alert_override`, created on first start. It holds the dashboard's alert switch and mute (with the mute reason, up to 200 characters). A downgrade ignores it.
- **New optional config key** `scheduleAlerts.alerts[].enabled` (default `true`; `false` switches that alert off permanently) and a new template key `scheduleAlerts.templates.muteEnded`. Nothing changes unless you use them.
- **Dashboard controls need writes.** The switch and the mute form work only with `dashboard.allowConfigWrites: true` and dashboard auth other than `none`; otherwise they are shown disabled.
- **Mute-end message.** After a mute ends, each destination receives one summary message if anything was held back; a mute with nothing to report ends silently.
## Upgrading to the Origin check release (#259)
- **Cross-origin browser POSTs are refused.** `sync`, `dismiss-error`, `reset-history` and `test-notification` now return 403 `origin_rejected` when a browser sends an `Origin` or `Referer` that is not same-origin and not in `dashboard.allowedOrigins`. curl, scripts and Home Assistant (no browser headers) are unaffected.
- **WARNING: `dashboard.allowedOrigins` now also authorizes these POSTs**, not only `/ws/logs`. Review the list and remove any origin you do not fully control.
- **Behind a reverse proxy**, if the dashboard buttons now fail with 403, the proxy is rewriting `Host`. Use `proxy_set_header Host $http_host;` (nginx), or add the page origin to `dashboard.allowedOrigins`.
- **Body errors are JSON.** Malformed or oversized bodies on these routes now return JSON (`invalid_json` 400, `payload_too_large` 413, `invalid_body` 400 for other unreadable bodies) instead of an HTML error page.
- New optional setting `dashboard.allowConfigWrites` (default `false`); nothing changes unless you enable it.
## Upgrading to 1.11.0
**Read this before upgrading if you use notifications.** Four changes alter behaviour for existing configs. Two can make a channel go *silent*, one can make a channel start *talking*, and one can stop the service from starting.
### Check your config first (30 seconds)
Validate against the new schema **before** you switch over, without touching your running service:
```bash
docker run --rm -v ./config:/app/config:ro \
ghcr.io/agigante80/actual-sync:latest npm run validate-config
```
Anything reported here is something that would otherwise fail at startup. (This check was itself broken in 1.11.0 and 1.11.1 — under a bind mount it silently skipped **schema** validation, so it reported configs as valid that the service then refused to start. Use a release later than 1.11.1 for it to be meaningful.)
### 1. `notifyOnSuccess` now actually works — and `never` silences failures too
`notifications.telegram.notifyOnSuccess` has existed in the schema for a long time but **gated nothing**: every channel notified on every sync regardless of what you set. From 1.11.0 it is enforced, and it now exists on every channel — with one exception, legacy `webhooks.telegram` entries, covered in §3 below.
The trap is the name. `notifyOnSuccess: "never"` reads like *"never notify me on success"* — but it means **"turn this channel off entirely, failures included"**.
| If you meant | Use |
|---|---|
| Stop telling me about *successful* syncs | **`errors_only`** |
| Turn this channel off completely | `never` |
**Action:** if any config of yours sets `notifyOnSuccess: "never"` and you actually wanted quiet successes, change it to `errors_only` before upgrading. On startup the service logs a `WARN` naming every channel it has muted, so check your logs after the first boot:
```
[WARN] Channels muted by notifyOnSuccess: "never" — these will NOT notify on failures either
```
This also applies to a mode set with the Telegram `/notify` command. Before 1.11.0 that command changed nothing on the dispatch path, so a value typed months ago as a no-op now takes effect. An explicit `notifications.telegram.notifyOnSuccess` in your config overrides a stored `/notify` value on restart; if config is silent, the stored value applies.
### 1b. Seeded your config from the example? Telegram gets quieter
This is the most widely-affected change, and it needs no action — but it will surprise you if you don't expect it.
Every `config.example.json` up to 1.10.1 shipped this:
```json
"telegram": { "notifyOnSuccess": "errors_only" }
```
It did nothing, because the setting gated nothing. If you seeded your config from the example and left that line alone, **Telegram now stops notifying on successful syncs** — which is what the line always claimed it did.
**Action:** none, if that is what you want. If you would rather keep every-sync notifications on Telegram, change it to `"always"` or delete the key. The example no longer sets it per-channel.
### 2. Enabled email now requires a sender and at least one recipient
`notifications.email.enabled: true` combined with a missing `from`, a missing `to`, or an empty `to: []` previously validated fine — and then silently delivered nothing. That is now **rejected at startup**:
```
Configuration is invalid — it does not match the schema:
- /notifications/email/to: must NOT have fewer than 1 items
```
**Action:** either fill in `from` and `to`, or set `enabled: false`. A disabled email block with an empty `to` is still perfectly valid, so placeholder stubs are unaffected.
If you need to boot before you can fix the config, `CONFIG_STRICT=false` downgrades schema failures to warnings. Use it to get running, not as a permanent setting — the channel still cannot deliver.
### 3. Legacy `webhooks.telegram` entries now actually deliver
A `notifications.webhooks.telegram` entry has never sent a message — the code path that would read it was unreachable. It works now.
Consequence: an entry has **no `enabled` flag — its presence is its enablement**. If you have a legacy entry *and* `notifications.telegram.enabled: false`, you were receiving nothing and will now start receiving messages.
**Action:** if you want that channel to stay off, either **remove the entry**, or set it on the top-level block:
```json
"notifications": {
"telegram": { "enabled": false, "notifyOnSuccess": "never" },
"webhooks": { "telegram": [ { "name": "legacy", "botToken": "…", "chatId": "…" } ] }
}
```
> **Do not** put `notifyOnSuccess` on the legacy entry itself. Legacy `webhooks.telegram` items are the one exception to "every channel supports it" — the schema has no such key there, and because an unknown key is only an advisory warning the service still starts and silently ignores it. The channel would keep sending.
### Also worth knowing
- **Delivery reporting is honest now.** The log line `Sync notification sent` only appears when at least one channel actually delivered; a total failure logs `Sync notification failed on every channel` at WARN. If you have monitoring that greps for the old line, it will now correctly go quiet when your alerting is broken.
- **Undelivered notifications no longer consume the rate-limit budget.** A broken transport is therefore retried on every scheduled sync instead of being rate-limited away. See [Rate Limiting](NOTIFICATIONS.md#rate-limiting).
- **A dead Telegram bot token is logged once**, not every two seconds.
Full reference: [docs/NOTIFICATIONS.md](NOTIFICATIONS.md#notification-mode-notifyonsuccess).
---
## Overview
As of December 2025, Actual-sync has moved from hardcoded server configuration to external configuration files. This provides better flexibility and security.
---
## What Changed
### Before (Hardcoded Configuration)
Server configuration was embedded in `sync_all_banks.js`:
```javascript
const servers = [
{
name: 'Main',
url: process.env.SERVICE_MAIN_URL || 'http://actual-main:5006',
password: process.env.SERVICE_MAIN_PASSWORD || 'hunter2',
// ...
}
];
```
### After (External Configuration)
Server configuration is now in `config.json`:
```json
{
"servers": [
{
"name": "Main",
"url": "http://actual-main:5006",
"password": "your_password",
"syncId": "your_sync_id",
"dataDir": "/app/dataDir_Main_temp"
}
],
"sync": {
"maxRetries": 5,
"baseRetryDelayMs": 3000,
"schedule": "03 03 */2 * *"
}
}
```
---
## Migration Steps
### Step 1: Install Dependencies
Update to the latest version and install new dependencies:
```bash
cd /path/to/Actual-sync
npm install
```
### Step 2: Create config.json
Copy the example configuration:
```bash
cp config/config.example.json config/config.json
```
### Step 3: Migrate Your Settings
If you were using environment variables, migrate them to `config.json`:
**Environment Variables → config.json Mapping:**
| Environment Variable | config.json Path |
|---------------------|------------------|
| `SERVICE_MAIN_URL` | `servers[0].url` |
| `SERVICE_MAIN_PASSWORD` | `servers[0].password` |
| `SERVICE_MAIN_SYNC_ID` | `servers[0].syncId` |
| `SERVICE_MAIN_DIR` | `servers[0].dataDir` |
| `SERVICE_ALEJANDRO_URL` | `servers[1].url` |
| `SERVICE_ALEJANDRO_PASSWORD` | `servers[1].password` |
| `SERVICE_ALEJANDRO_SYNC_ID` | `servers[1].syncId` |
| `SERVICE_ALEJANDRO_DIR` | `servers[1].dataDir` |
**Example Migration:**
If your `.env` file had:
```env
SERVICE_MAIN_URL=https://budget.example.com
SERVICE_MAIN_PASSWORD=mySecurePassword123
SERVICE_MAIN_SYNC_ID=abc-def-ghi-123
SERVICE_MAIN_DIR=/data/main
```
Your `config.json` should have:
```json
{
"servers": [
{
"name": "Main",
"url": "https://budget.example.com",
"password": "mySecurePassword123",
"syncId": "abc-def-ghi-123",
"dataDir": "/data/main"
}
]
}
```
### Step 4: Configure Multiple Servers
To add more servers, add entries to the `servers` array:
```json
{
"servers": [
{
"name": "Personal",
"url": "https://personal.example.com",
"password": "password1",
"syncId": "sync-id-1",
"dataDir": "/data/personal"
},
{
"name": "Business",
"url": "https://business.example.com",
"password": "password2",
"syncId": "sync-id-2",
"dataDir": "/data/business"
}
]
}
```
### Step 5: Adjust Sync Settings (Optional)
Customize retry behavior and schedule:
```json
{
"sync": {
"maxRetries": 5, // Number of retry attempts (0-10)
"baseRetryDelayMs": 3000, // Base delay for exponential backoff (milliseconds)
"schedule": "0 2 * * *" // Cron: Run at 2 AM daily
}
}
```
**Common Cron Schedules:**
- Every day at 2 AM: `0 2 * * *`
- Every 6 hours: `0 */6 * * *`
- Every other day at 3:03 AM: `03 03 */2 * *` (default)
- Every hour: `0 * * * *`
### Step 6: Secure Your Configuration
**Important Security Steps:**
1. Set appropriate file permissions:
```bash
chmod 600 config.json
```
2. Verify `config.json` is in `.gitignore`:
```bash
grep "config.json" .gitignore
```
3. Never commit `config.json` to version control
### Step 7: Validate Configuration
Test your configuration before running:
```bash
npm run validate-config
```
Or just start the service (it validates on startup):
```bash
npm start
```
### Step 8: Test Sync
Run a manual sync to verify everything works:
```bash
npm run sync
```
---
## Troubleshooting
### "Configuration file not found"
**Error:**
```
❌ Failed to load configuration:
Configuration file not found: ./config.json
```
**Solution:**
Create `config/config.json` from the example:
```bash
cp config/config.example.json config/config.json
```
Then edit with your settings.
---
### "Invalid JSON in configuration file"
**Error:**
```
❌ Failed to load configuration:
Invalid JSON in configuration file: Unexpected token...
```
**Solution:**
- Check for missing commas between properties
- Check for trailing commas (not allowed in JSON)
- Validate JSON syntax: https://jsonlint.com/
- Use a JSON validator:
```bash
node -e "JSON.parse(require('fs').readFileSync('config/config.json', 'utf8'))"
```
---
### "Configuration validation failed"
**Error:**
```
❌ Failed to load configuration:
Configuration validation failed:
- /servers/0/password: must be string
```
**Solution:**
Fix the specific field mentioned in the error. Common issues:
- Missing required fields (name, url, password, syncId, dataDir)
- Wrong data types (e.g., number instead of string)
- Invalid URLs (must start with http:// or https://)
---
### Warning: "Using unencrypted HTTP connection"
**Warning:**
```
⚠️ Warning: Server "Main" uses unencrypted HTTP connection.
Consider using HTTPS for production: http://example.com
```
**Solution:**
This is a warning, not an error. Consider using HTTPS in production:
```json
{
"url": "https://example.com" // Use HTTPS instead of HTTP
}
```
For local/internal servers (localhost, 127.0.0.1), HTTP is acceptable.
---
### Warning: "Weak password"
**Warning:**
```
⚠️ Warning: Server "Main" has a weak password (< 8 characters).
Consider using a stronger password for security.
```
**Solution:**
Use a longer, more secure password (16+ characters recommended):
```json
{
"password": "aVeryLongAndSecurePassword123!@#"
}
```
---
## Backward Compatibility
### Environment Variables Still Work
The `.env` file is still loaded, but it's no longer used for server configuration. You can remove the old environment variables:
```bash
# These are no longer needed:
# SERVICE_MAIN_URL
# SERVICE_MAIN_PASSWORD
# SERVICE_MAIN_SYNC_ID
# SERVICE_MAIN_DIR
# SERVICE_ALEJANDRO_URL
# SERVICE_ALEJANDRO_PASSWORD
# SERVICE_ALEJANDRO_SYNC_ID
# SERVICE_ALEJANDRO_DIR
```
### Rolling Back
If you need to roll back to the old version:
1. Check out the previous version:
```bash
git checkout <previous-commit>
```
2. Reinstall dependencies:
```bash
npm install
```
Your `.env` file will work again with the old version.
---
## Configuration Reference
### Complete Example
```json
{
"$schema": "./config.schema.json",
"servers": [
{
"name": "Production",
"url": "https://budget.example.com",
"password": "SecurePassword123!",
"syncId": "abc-123-def-456",
"dataDir": "/var/lib/actual-sync/production"
},
{
"name": "Testing",
"url": "http://localhost:5006",
"password": "TestPassword",
"syncId": "test-sync-id",
"dataDir": "/tmp/actual-sync-test"
}
],
"sync": {
"maxRetries": 5,
"baseRetryDelayMs": 3000,
"schedule": "0 2 * * *"
},
"logging": {
"level": "info"
}
}
```
### Field Descriptions
**servers** (required, array):
- **name** (required, string): Human-readable server name
- **url** (required, string): Actual Budget server URL
- **password** (required, string): Server password
- **syncId** (required, string): Budget sync ID from Actual settings
- **dataDir** (required, string): Local directory for budget cache
**sync** (optional, object):
- **maxRetries** (optional, integer, default: 5): Max retry attempts (0-10)
- **baseRetryDelayMs** (optional, integer, default: 3000): Base retry delay in ms
- **schedule** (optional, string, default: "03 03 */2 * *"): Cron expression
**logging** (optional, object):
- **level** (optional, string, default: "info"): Log level (debug/info/warn/error)
---
## Getting Help
If you encounter issues during migration:
1. Check the [troubleshooting section](#troubleshooting)
2. Validate your JSON syntax
3. Review the example configuration: `config.example.json`
4. Check the schema: `config.schema.json`
5. Open an issue with your error message (remove sensitive data!)
---
**Migration completed?** You're all set — your budgets now sync from `config.json`.