This commit is contained in:
seb
2026-10-16 13:59:02 +02:00
parent e083d66a75
commit bed19f8b10
7 changed files with 313 additions and 6 deletions

View File

@@ -265,9 +265,11 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
| [`routes/pos/pimage.md`](routes/pos/pimage.md) | `GET /v1/pimage` |
| [`routes/v1/product.md`](routes/v1/product.md) | `GET /v1/product` |
| [`routes/vfs/list.md`](routes/vfs/list.md) | `GET /vfs` |
| [`routes/vfs/applets-restore.md`](routes/vfs/applets-restore.md) | `POST /vfs/applets/restore` |
| [`routes/vfs/drafts/response.md`](routes/vfs/drafts/response.md) | `POST /vfs/drafts/response` |
| [`routes/vfs/mkdir.md`](routes/vfs/mkdir.md) | `POST /vfs/mkdir` |
| [`routes/vfs/rename.md`](routes/vfs/rename.md) | `POST /vfs/rename` |
| [`routes/vfs/skills-restore.md`](routes/vfs/skills-restore.md) | `POST /vfs/skills/restore` |
| [`routes/vfs/upload.md`](routes/vfs/upload.md) | `POST /vfs/upload` |
| [`routes/vfs/delete.md`](routes/vfs/delete.md) | `DELETE /vfs/{*path}` |
| [`routes/vfs/_*path.md`](routes/vfs/_*path.md) | `GET /vfs/{*path}` |
@@ -305,7 +307,7 @@ Clients subscribe via [`GET /events`](routes/events.md) (`text/event-stream`). A
| `quote` | `DELETE /quote/{id}/file/{fileId}`, `POST /quote/{id}/pdf` |
| `shipment` | `PATCH /shipment/{id}`, `DELETE /shipment/{id}/file/{fileId}`, `POST /shipment/{id}/pdf` |
| `stock` | `POST /stock/adjust` |
| `vfs` | `POST /vfs/mkdir`, `POST /vfs/rename`, `POST /vfs/upload`, `DELETE /vfs/{*path}`, `POST /vfs/{*path}`, `PUT /vfs/{*path}` |
| `vfs` | `POST /vfs/applets/restore`, `POST /vfs/mkdir`, `POST /vfs/rename`, `POST /vfs/skills/restore`, `POST /vfs/upload`, `DELETE /vfs/{*path}`, `POST /vfs/{*path}`, `PUT /vfs/{*path}` |
| `warehouse` | `POST /warehouse`, `PUT /warehouse/order`, `DELETE /warehouse/{id}`, `PUT /warehouse/{id}` |
## DESIGN

View File

@@ -1,6 +1,6 @@
# Dash API Routes Overview (All)
Overview of all 248 API endpoints across all modules.
Overview of all 250 API endpoints across all modules.
### Notation
@@ -912,6 +912,9 @@ _none_
#### List virtual filesystem files — `GET /vfs`
_none_
- Returns: `200 { files: { name: str, size: int, mime: str, updated_at: str, created_at: str, isText: bool, isImage: bool, isPdf: bool }[] }`
#### Restore system default applets — `POST /vfs/applets/restore`
_none_
- Returns: `200 { success: bool, restored: str[], count: int }`
#### Respond to a frontend draft request — `POST /vfs/drafts/response`
- Params: `body: { requestId: str, content?: any, error?: str, files?: any[] }`
- Returns: `200 { success: bool }`
@@ -921,6 +924,9 @@ _none_
#### Rename or move a virtual filesystem file or directory — `POST /vfs/rename`
- Params: `body: { oldPath: str, newPath: str }`
- Returns: `200 { success: bool, oldPath: str, newPath: str }`
#### Restore system default skills — `POST /vfs/skills/restore`
_none_
- Returns: `200 { success: bool, restored: str[], count: int }`
#### Upload files to virtual filesystem — `POST /vfs/upload`
- Params: `form: { files: str[], targetDir?: str }`
- Returns: `200 { success: bool, uploaded: { name: str, size: int, mime: str }[] }`

View File

@@ -0,0 +1,40 @@
# `POST /vfs/applets/restore`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Restore system default applets
Restores all shipped system default example applets (.applet.js) into the active virtual
filesystem (VFS) for the authenticated user's selected company, overwriting existing system applets with their upstream definitions.
## Signals
On success, emits SSE signal: `vfs` (see [`GET /events`](../events.md)).
## Response `200`
System applets restored successfully
```json
{
"success": true,
"restored": [
"…"
],
"count": 1
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | No company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 500 | Restore failed — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,40 @@
# `POST /vfs/skills/restore`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Restore system default skills
Restores all shipped system default SKILL files into the active virtual filesystem (VFS)
for the authenticated user's selected company, overwriting existing system skills with their upstream definitions.
## Signals
On success, emits SSE signal: `vfs` (see [`GET /events`](../events.md)).
## Response `200`
System skills restored successfully
```json
{
"success": true,
"restored": [
"…"
],
"count": 1
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | No company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 500 | Restore failed — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -2,7 +2,14 @@ import fs from 'fs';
import path from 'path';
import multer from 'multer';
import { emit } from '../sse.js';
import { prepareVfs, resolveVfsRoot, isReadOnlyDatabasePath, getMimeType } from '../../tools/vfs.js';
import {
prepareVfs,
resolveVfsRoot,
isReadOnlyDatabasePath,
getMimeType,
restoreDefaultSkills,
restoreDefaultApplets,
} from '../../tools/vfs.js';
import sharp from 'sharp';
import {
parseDraftPath,
@@ -1464,4 +1471,162 @@ export function register(app, basePath = '') {
});
// #endregion
/**
* @openapi
* /vfs/skills/restore:
* post:
* tags: [Vfs]
* summary: Restore system default skills
* description: |
* Restores all shipped system default SKILL files into the active virtual filesystem (VFS)
* for the authenticated user's selected company, overwriting existing system skills with their upstream definitions.
* security:
* - cookieAuth: []
* x-signals: [vfs]
* x-markdown: vfs/skills-restore.md
* responses:
* 200:
* description: System skills restored successfully
* content:
* application/json:
* schema:
* type: object
* required: [success, restored, count]
* properties:
* success:
* type: boolean
* restored:
* type: array
* items:
* type: string
* count:
* type: integer
* 400:
* description: No company selected
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 403:
* description: Not authorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Restore failed
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region POST /vfs/skills/restore
app.post(`${basePath}/vfs/skills/restore`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
try {
const restored = await restoreDefaultSkills({ googleAuthId: req.user.id });
emit('vfs', { companyId: selected.companyId });
return res.json({
success: true,
restored,
count: restored.length,
});
} catch (error) {
const status = vfsFileErrorStatus(error);
if (status === 500) console.error('Restore default skills failed:', error);
return res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /vfs/applets/restore:
* post:
* tags: [Vfs]
* summary: Restore system default applets
* description: |
* Restores all shipped system default example applets (.applet.js) into the active virtual
* filesystem (VFS) for the authenticated user's selected company, overwriting existing system applets with their upstream definitions.
* security:
* - cookieAuth: []
* x-signals: [vfs]
* x-markdown: vfs/applets-restore.md
* responses:
* 200:
* description: System applets restored successfully
* content:
* application/json:
* schema:
* type: object
* required: [success, restored, count]
* properties:
* success:
* type: boolean
* restored:
* type: array
* items:
* type: string
* count:
* type: integer
* 400:
* description: No company selected
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 403:
* description: Not authorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Restore failed
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region POST /vfs/applets/restore
app.post(`${basePath}/vfs/applets/restore`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
try {
const restored = await restoreDefaultApplets({ googleAuthId: req.user.id });
emit('vfs', { companyId: selected.companyId });
return res.json({
success: true,
restored,
count: restored.length,
});
} catch (error) {
const status = vfsFileErrorStatus(error);
if (status === 500) console.error('Restore default applets failed:', error);
return res.status(status).json({ error: error.message });
}
});
// #endregion
}

View File

@@ -10,7 +10,7 @@ import { parseVfsRef, vfsFileErrorStatus } from './vfs.js';
// VFS paths that are owned by their own literal route in `vfs.js`. They are registered as
// separate Express handlers, so this wildcard must defer to them instead of treating e.g.
// `/vfs/upload` as an applet path. Deferring keeps the two files order-independent.
const RESERVED_VFS_SEGMENTS = new Set(['upload', 'mkdir', 'rename']);
const RESERVED_VFS_SEGMENTS = new Set(['upload', 'mkdir', 'rename', 'drafts', 'skills', 'applets']);
function requireSelectedCompany(req) {
const companyId = req.user?.selectedCompanyId;

View File

@@ -162,8 +162,8 @@ export async function loadDefaultSkills({ googleAuthId, store: passedStore, vfsR
if (!fs.existsSync(diskPath)) {
fs.mkdirSync(path.dirname(diskPath), { recursive: true });
fs.writeFileSync(diskPath, skill.content, 'utf8');
loaded.push(skill.name);
}
loaded.push(skill.name);
} catch (err) {
console.error(`[vfs] failed to write default skill "${skill.name}" to disk:`, err.message);
}
@@ -172,6 +172,34 @@ export async function loadDefaultSkills({ googleAuthId, store: passedStore, vfsR
return loaded;
}
/**
* Restores all shipped system default SKILL files into the company's VFS.
*
* @param {{ googleAuthId: number, store?: object, vfsRoot?: string }} ctx
* @returns {Promise<Array<string>>} List of restored skill file names
*/
export async function restoreDefaultSkills({ googleAuthId, store: passedStore, vfsRoot: passedVfsRoot } = {}) {
const store = passedStore || await getStore();
const vfsRoot = passedVfsRoot || await vfsRootFor(store, googleAuthId);
fs.mkdirSync(vfsRoot, { recursive: true });
const defaultSkills = getDefaultSkills();
const restored = [];
for (const skill of defaultSkills) {
try {
const diskPath = path.resolve(vfsRoot, skill.name);
fs.mkdirSync(path.dirname(diskPath), { recursive: true });
fs.writeFileSync(diskPath, skill.content, 'utf8');
restored.push(skill.name);
} catch (err) {
console.error(`[vfs] failed to restore default skill "${skill.name}":`, err.message);
}
}
return restored;
}
/**
* Loads the shipped example `.applet.js` files into the selected company's VFS.
*
@@ -194,8 +222,8 @@ export async function loadDefaultApplets({ googleAuthId, store: passedStore, vfs
const diskPath = path.resolve(vfsRoot, name);
if (!fs.existsSync(diskPath)) {
fs.writeFileSync(diskPath, applet.content, 'utf8');
seeded.push(name);
}
seeded.push(name);
} catch (err) {
console.error(`[vfs] failed to write default applet "${name}" to disk:`, err.message);
}
@@ -204,6 +232,32 @@ export async function loadDefaultApplets({ googleAuthId, store: passedStore, vfs
return seeded;
}
/**
* Restores all shipped system default example applets into the company's VFS.
*
* @param {{ googleAuthId: number, store?: object, vfsRoot?: string }} ctx
* @returns {Promise<Array<string>>} List of restored applet file names
*/
export async function restoreDefaultApplets({ googleAuthId, store: passedStore, vfsRoot: passedVfsRoot } = {}) {
const store = passedStore || await getStore();
const vfsRoot = passedVfsRoot || await vfsRootFor(store, googleAuthId);
fs.mkdirSync(vfsRoot, { recursive: true });
const restored = [];
for (const applet of getDefaultApplets()) {
const name = `${DEFAULT_APPLETS_VERSION}-${applet.name}`;
try {
const diskPath = path.resolve(vfsRoot, name);
fs.writeFileSync(diskPath, applet.content, 'utf8');
restored.push(name);
} catch (err) {
console.error(`[vfs] failed to restore default applet "${name}":`, err.message);
}
}
return restored;
}
/**
* Initializes the VFS for the selected company with default SKILL files and example applets.
*