Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions api/_lib/handlers/license-validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ module.exports = async (req, res) => {
return res.status(405).json({ error: 'Method Not Allowed' });
}

const { licenseKey, instanceId } = req.body || {};
const { licenseKey, instanceId, deviceName } = req.body || {};
if (!licenseKey) {
return res.status(400).json({ error: 'licenseKey is required' });
}
Expand Down Expand Up @@ -79,13 +79,13 @@ module.exports = async (req, res) => {
});
}
try {
await store.bindDevice(key, instanceId, { source: 'validate' });
await store.bindDevice(key, instanceId, { source: 'validate', deviceName: deviceName ? String(deviceName).trim() : undefined });
} catch (err) {
console.error('validate: failed to bind instance', err);
}
} else {
try {
await store.bindDevice(key, instanceId, { source: 'validate' });
await store.bindDevice(key, instanceId, { source: 'validate', deviceName: deviceName ? String(deviceName).trim() : undefined });
} catch (err) {
console.error('validate: failed to refresh device', err);
}
Expand Down
25 changes: 24 additions & 1 deletion api/_lib/handlers/sync-devices.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// GET /api/sync/devices — list devices for the signed-in account.
// PATCH /api/sync/devices/:deviceId — rename this device (body: { device_name }).
// DELETE /api/sync/devices/:deviceId — revoke a device roster entry.

const { authenticateBearer } = require('../sync-auth');
const { listDevices, revokeDevice } = require('../sync-db');
const { listDevices, revokeDevice, updateDeviceName } = require('../sync-db');

module.exports = async (req, res) => {
let auth;
Expand Down Expand Up @@ -49,5 +50,27 @@ module.exports = async (req, res) => {
}
}

if (req.method === 'PATCH') {
const deviceId = req.query.deviceId;
const deviceName = req.body?.device_name;
if (!deviceId) {
return res.status(400).json({ error: 'deviceId is required' });
}
const callerDeviceId = req.headers['x-device-id'] || req.headers['X-Device-Id'];
if (!callerDeviceId || String(callerDeviceId) !== String(deviceId)) {
return res.status(403).json({ error: 'Can only rename this device' });
}
try {
const ok = await updateDeviceName(auth.account_id, String(deviceId), deviceName);
if (!ok) {
return res.status(400).json({ error: 'device_name is required' });
}
return res.status(200).json({ ok: true, device_id: String(deviceId), device_name: String(deviceName).trim() });
} catch (err) {
console.error('sync/devices PATCH:', err);
return res.status(500).json({ error: 'Failed to rename device' });
}
}

return res.status(405).json({ error: 'Method Not Allowed' });
};
7 changes: 5 additions & 2 deletions api/_lib/handlers/sync-quota.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
const { authenticateBearer } = require('../sync-auth');
const { getAccountQuota } = require('../sync-db');

const CLOUD_QUOTA_MB = 100;
const BYTES_PER_MB = 1024 * 1024;

const QUOTA_BYTES = {
sponsor: 500 * 1024 * 1024,
singularity: 5 * 1024 * 1024 * 1024,
sponsor: CLOUD_QUOTA_MB * BYTES_PER_MB,
singularity: CLOUD_QUOTA_MB * BYTES_PER_MB,
};

module.exports = async (req, res) => {
Expand Down
5 changes: 4 additions & 1 deletion api/_lib/license-db.js
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,10 @@ async function bindDevice(licenseKey, instanceId, meta = {}) {
ON CONFLICT (license_key, instance_id) DO UPDATE SET
last_seen = now(),
revoked_at = NULL,
device_name = COALESCE(EXCLUDED.device_name, pgstudio_license.devices.device_name)
device_name = CASE
WHEN EXCLUDED.device_name IS NOT NULL THEN EXCLUDED.device_name
ELSE pgstudio_license.devices.device_name
END
`;

const source = meta.source || 'validate';
Expand Down
18 changes: 17 additions & 1 deletion api/_lib/sync-db.js
Original file line number Diff line number Diff line change
Expand Up @@ -423,11 +423,26 @@ async function upsertDevice(accountId, deviceId, deviceName) {
INSERT INTO pgstudio_sync.sync_devices (account_id, device_id, device_name, last_seen)
VALUES (${accountId}, ${deviceId}, ${deviceName || null}, now())
ON CONFLICT (account_id, device_id) DO UPDATE SET
device_name = COALESCE(EXCLUDED.device_name, pgstudio_sync.sync_devices.device_name),
device_name = CASE
WHEN EXCLUDED.device_name IS NOT NULL THEN EXCLUDED.device_name
ELSE pgstudio_sync.sync_devices.device_name
END,
last_seen = now()
`;
}

async function updateDeviceName(accountId, deviceId, deviceName) {
if (!deviceId) {
return false;
}
const trimmed = String(deviceName || '').trim();
if (!trimmed) {
return false;
}
await upsertDevice(accountId, deviceId, trimmed);
return true;
}

async function listDevices(accountId) {
await ensureSchema();
const db = getSql();
Expand Down Expand Up @@ -532,6 +547,7 @@ module.exports = {
getAccountQuota,
setAccountTier,
upsertDevice,
updateDeviceName,
listDevices,
revokeDevice,
markAccountInactive,
Expand Down
6 changes: 4 additions & 2 deletions docs/CLOUD_SYNC.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ On the free plan the backup is bound to the first device that syncs. A different

| Item | Included | Notes |
|------|----------|-------|
| Connection profiles | Yes (default) | Host, port, user, database, SSL mode, environment tag, SSH host settings |
| Connection profiles | Yes (default) | Host, port, user, database, SSL mode, environment tag, **connection group**, SSH host settings |
| Saved queries | Yes (default) | Full query text and metadata |
| Notebooks (`.pgsql`) | Yes (default) | **Cells only** — SQL and markdown content, not execution outputs |
| Notebooks (`.pgsql`) | Yes (default) | **Cells only** — SQL and markdown content, not execution outputs; **folder path** and connection display name for cross-device layout |
| Passwords | Opt-in only | Encrypted secrets bundle; off by default |

Sync uses a merge model: changes from any signed-in device are combined. Conflicts are surfaced in the status bar.

**Notebook folders:** Each synced notebook carries a relative `folderPath` (parent directories under extension storage) plus `connectionName` for fallback layout. On pull, connections are applied before notebooks so group labels and folder names match the source device. You can reorganize notebooks locally (move/rename folders); the next sync propagates the new layout. If two devices move the same notebook differently, last push wins for folder placement.

---

## What does **not** sync
Expand Down
76 changes: 76 additions & 0 deletions docs/DESIGN_SYSTEM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# PgStudio Design System

Unified visual language for webview panels, tree views, and notebook renderer output.

## Token layers

| Layer | Location | Consumed by |
|-------|----------|-------------|
| `--pg-*` tokens | `templates/shared/styles.css` | All webview panels via `readSharedTemplateCss()` |
| Component classes | `templates/shared/components.css` | Panels using `prependSharedTemplateCss` / `loadPanelTemplate` |
| `MODERN_WEBVIEW_BASE_CSS` | `src/common/htmlStyles.ts` | Injected before shared CSS in panel loaders |
| Renderer mirrors | `src/ui/renderer/rendererConstants.ts` | Notebook renderer only (`--vscode-*` vars) |
| Tree colors | `package.json` → `contributes.colors` | `ThemeColor('postgres.*')` in tree providers |

## Token catalog (webview)

### Surfaces
- `--pg-ui-surface`, `--pg-ui-surface-raised`, `--pg-ui-surface-muted`
- `--pg-ui-border`, `--pg-ui-border-strong`, `--pg-ui-hover`

### Brand & accent
- `--pg-brand-accent`, `--pg-brand-accent-muted`
- `--pg-accent-gradient` — subtle radial highlight on cards

### Glass & elevation
- `--pg-glass-bg`, `--pg-glass-blur` — **floating elements only**
- `--pg-elevation-1` / `-2` / `-3` — layered shadows

### Environment semantics
- `--env-prod`, `--env-staging`, `--env-dev`

## Selective glass rule

| Use blur (`backdrop-filter`) | No blur (raised surface + shadow) |
|------------------------------|-----------------------------------|
| `.pg-modal-backdrop`, `.pg-toast` | `.pg-card`, `.pg-hero-card`, fields, tables |
| Renderer hover toolbars, sentinel top bar | Result container body, tab panels |

## Component classes

| Class | Purpose |
|-------|---------|
| `.pg-card` / `.pg-card-header` / `.pg-card-body` | Standard content card |
| `.pg-hero-card` | Featured card with gradient top bar |
| `.pg-badge`, `.pg-pill`, `.pg-status-dot` | Status & tags |
| `.pg-tab-strip` / `.pg-tab-btn` / `.pg-tab-panel` | Tab navigation |
| `.pg-btn`, `.pg-btn-sm`, `.pg-btn-danger` | Buttons (extends shared `.pg-btn--*`) |
| `.pg-empty-state` | Empty / zero-data states |
| `.pg-modal-backdrop` / `.pg-modal` | Dialogs |
| `.pg-toast` | Floating notifications |

Settings Hub retains `.hub-*` classes; its `--hub-*` tokens alias `--pg-*` for backward compatibility.

## When to use which loader

| Pattern | Use when |
|---------|----------|
| `loadPanelTemplate(webview, uri, folder, vars)` | New/simple panels with `index.html` + CSP + nonce |
| `loadCompleteTemplate(uri, folder, vars)` | Panels with `{{STYLES}}`/`{{SCRIPTS}}` placeholders only |
| Manual load (Settings Hub, Chat, Dashboard) | Complex variable injection, external script URIs, or legacy templates |

All panel loaders should prepend `MODERN_WEBVIEW_BASE_CSS` + `readSharedTemplateCss()` (includes `components.css`).

## Tree icon palette

Centralized in `src/providers/tree/treeIconTheme.ts`. Override brand accent:

```json
"workbench.colorCustomizations": {
"postgres.accent": "#7c3aed"
}
```

## Renderer notes

The notebook renderer sandbox cannot read `--pg-*`. Mirror needed values in `rendererConstants.ts` using `--vscode-*` and `color-mix`. Chart.js palettes resolve at runtime via `getThemeChartPalette()`.
Loading
Loading