diff --git a/.agents/skills/backend/SKILL.md b/.agents/skills/backend/SKILL.md
index bdaf042..7e335b7 100644
--- a/.agents/skills/backend/SKILL.md
+++ b/.agents/skills/backend/SKILL.md
@@ -7,7 +7,7 @@ description: >-
# Dash API Routes Overview (All)
-Overview of all 248 API endpoints across all modules.
+Overview of all 250 API endpoints across all modules.
### Notation
@@ -919,6 +919,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 }`
@@ -928,6 +931,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 }[] }`
diff --git a/src/api/vfs.js b/src/api/vfs.js
index aacf7db..270fcaa 100644
--- a/src/api/vfs.js
+++ b/src/api/vfs.js
@@ -181,4 +181,32 @@ export const vfs = {
}
},
// #endregion
+
+ /**
+ * POST /api/vfs/skills/restore
+ * Restores all system default .SKILL files into the company's VFS (overwrites existing files).
+ * Requires no request body. Emits vfs SSE event.
+ * → 200 { success: true, restored: string[], count: number }
+ */
+ // #region POST /api/vfs/skills/restore — restoreSkills
+ restoreSkills() {
+ return http('/api/vfs/skills/restore', {
+ method: 'POST',
+ });
+ },
+ // #endregion
+
+ /**
+ * POST /api/vfs/applets/restore
+ * Restores all system default .applet.js example applets into the company's VFS (overwrites existing files).
+ * Requires no request body. Emits vfs SSE event.
+ * → 200 { success: true, restored: string[], count: number }
+ */
+ // #region POST /api/vfs/applets/restore — restoreApplets
+ restoreApplets() {
+ return http('/api/vfs/applets/restore', {
+ method: 'POST',
+ });
+ },
+ // #endregion
};
diff --git a/src/automats/chatAutomat.js b/src/automats/chatAutomat.js
index 524ee87..d93fda5 100644
--- a/src/automats/chatAutomat.js
+++ b/src/automats/chatAutomat.js
@@ -331,20 +331,18 @@ const actions = {
/**
* Stage an extracted text snippet (e.g. from an invoice or delivery document) as a pill.
- * @param {{ text: string, size?: number, label?: string }} snippet
+ * @param {{ text: string, label?: string }} snippet
*/
- insertExtractionSnippet({ text = '', size, label } = {}) {
+ insertExtractionSnippet({ text = '', label = 'parsed invoice' } = {}) {
const trimmed = String(text || '').trim();
if (!trimmed) return;
- const finalSize = Number(size) || new Blob([trimmed]).size;
chatOpenedThisSession = true;
chatAutomat.setState({
isOpen: true,
sseEstablished: true,
pendingExtraction: {
text: trimmed,
- size: finalSize,
- label,
+ label: label || 'parsed invoice',
},
});
ensureCompanyEventsConnected();
@@ -356,26 +354,17 @@ const actions = {
/**
* Open the chat drawer and append text / prompt to the chat composer.
- * If the prompt is an extracted text snippet (e.g. `Text aus „...“:`),
- * automatically converts it into a staged extraction pill.
* @param {string} prompt
*/
insertPrompt(prompt = '') {
- chatOpenedThisSession = true;
- const trimmedPrompt = String(prompt || '').trim();
- const extractionMatch = trimmedPrompt.match(/^(?:Text for Data Extraction|Text aus „[^“]+“):\s*```(?:\w+)?\n([\s\S]*?)```$/);
- if (extractionMatch) {
- const content = (extractionMatch[1] || '').trim();
- if (content) {
- chatAutomat.actions.insertExtractionSnippet({ text: content });
- return;
- }
- }
+ const text = String(prompt || '').trim();
+ if (!text) return;
+ chatOpenedThisSession = true;
chatAutomat.setState((prev) => ({
isOpen: true,
sseEstablished: true,
- pendingInput: prev.pendingInput ? `${prev.pendingInput}\n\n${prompt}` : prompt,
+ pendingInput: prev.pendingInput ? `${prev.pendingInput}\n\n${text}` : text,
}));
ensureCompanyEventsConnected();
if (!chatAutomat.state.selectedConversationId && !chatAutomat.state.draftConversation) {
diff --git a/src/components/chat/ChatDrawer.jsx b/src/components/chat/ChatDrawer.jsx
index 0ac0bbd..a9ce16d 100644
--- a/src/components/chat/ChatDrawer.jsx
+++ b/src/components/chat/ChatDrawer.jsx
@@ -15,6 +15,7 @@ import SendRoundedIcon from '@mui/icons-material/SendRounded';
import AddRoundedIcon from '@mui/icons-material/AddRounded';
import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded';
import DeleteOutlineRoundedIcon from '@mui/icons-material/DeleteOutlineRounded';
+import DescriptionOutlinedIcon from '@mui/icons-material/DescriptionOutlined';
import Tooltip from '@mui/material/Tooltip';
import Badge from '@mui/material/Badge';
@@ -115,6 +116,15 @@ export default class ChatDrawer extends PureComponent {
isInputFocused: false,
stagedAttachments: [],
stagedReferences: [],
+ stagedExtractions: s.pendingExtraction
+ ? [
+ {
+ id: `ext-${Date.now()}`,
+ text: s.pendingExtraction.text,
+ label: s.pendingExtraction.label || 'parsed invoice',
+ },
+ ]
+ : [],
isProcessingFiles: false,
isDraggingFiles: false,
attachmentError: null,
@@ -178,6 +188,13 @@ export default class ChatDrawer extends PureComponent {
}
});
}
+ if (chatAutomat.state.pendingExtraction != null) {
+ queueMicrotask(() => {
+ if (chatAutomat.state.pendingExtraction != null) {
+ chatAutomat.setState({ pendingExtraction: null });
+ }
+ });
+ }
this.unsub = chatAutomat.subscribe(this, (state) => {
const wasOpen = this.state.isOpen;
const willOpen = state.isOpen;
@@ -211,6 +228,21 @@ export default class ChatDrawer extends PureComponent {
} else if (!wasOpen && willOpen) {
this.focusInput(0);
}
+ if (state.pendingExtraction != null) {
+ const ext = state.pendingExtraction;
+ const newExt = {
+ id: `ext-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
+ text: ext.text,
+ label: ext.label || 'parsed invoice',
+ };
+ patch.stagedExtractions = [...(this.state.stagedExtractions || []), newExt];
+ queueMicrotask(() => {
+ if (chatAutomat.state.pendingExtraction != null) {
+ chatAutomat.setState({ pendingExtraction: null });
+ }
+ });
+ this.focusInput(0);
+ }
return patch;
});
@@ -309,6 +341,13 @@ export default class ChatDrawer extends PureComponent {
this.focusInput(50);
};
+ handleRemoveExtraction = (id) => {
+ this.setState((prev) => ({
+ stagedExtractions: (prev.stagedExtractions || []).filter((e) => e.id !== id),
+ }));
+ this.focusInput(50);
+ };
+
handleVfsResizerStart = (e) => {
if (e.button !== undefined && e.button !== 0) return;
e.preventDefault();
@@ -483,9 +522,10 @@ export default class ChatDrawer extends PureComponent {
const rawText = this.state.inputText.trim();
const attachments = this.state.stagedAttachments || [];
const references = this.state.stagedReferences || [];
+ const extractions = this.state.stagedExtractions || [];
if (
- (!rawText && attachments.length === 0 && references.length === 0) ||
+ (!rawText && attachments.length === 0 && references.length === 0 && extractions.length === 0) ||
this.state.conversationState === 'busy' ||
this.state.isSending
) {
@@ -493,6 +533,13 @@ export default class ChatDrawer extends PureComponent {
}
let text = rawText;
+ if (extractions.length > 0) {
+ const extMarkdown = extractions
+ .map((ext) => `\`\`\`\n${ext.text.trim()}\n\`\`\``)
+ .join('\n\n');
+ text = text ? `${extMarkdown}\n\n${text}` : extMarkdown;
+ }
+
if (references.length > 0) {
const refMarkdown = references
.map((ref) => {
@@ -522,6 +569,7 @@ export default class ChatDrawer extends PureComponent {
inputText: '',
stagedAttachments: [],
stagedReferences: [],
+ stagedExtractions: [],
attachmentError: null,
});
@@ -1265,6 +1313,7 @@ export default class ChatDrawer extends PureComponent {
sendError,
stagedAttachments,
stagedReferences,
+ stagedExtractions = [],
isProcessingFiles,
isDraggingFiles,
attachmentError,
@@ -1277,7 +1326,10 @@ export default class ChatDrawer extends PureComponent {
const canSend =
!isWorking &&
canChat &&
- (Boolean(inputText.trim()) || stagedAttachments.length > 0 || stagedReferences.length > 0);
+ (Boolean(inputText.trim()) ||
+ stagedAttachments.length > 0 ||
+ stagedReferences.length > 0 ||
+ stagedExtractions.length > 0);
return (
- {stagedReferences.length > 0 && (
+ {(stagedReferences.length > 0 || (stagedExtractions && stagedExtractions.length > 0)) && (
))}
+ {stagedExtractions && stagedExtractions.map((ext) => (
+
+ theme.palette.mode === 'dark'
+ ? 'rgba(255, 255, 255, 0.08)'
+ : 'rgba(0, 0, 0, 0.05)',
+ border: '1px solid',
+ borderColor: (theme) =>
+ theme.palette.mode === 'dark'
+ ? 'rgba(255, 255, 255, 0.12)'
+ : 'rgba(0, 0, 0, 0.1)',
+ color: 'text.primary',
+ fontSize: '0.75rem',
+ lineHeight: 1.2,
+ userSelect: 'none',
+ }}
+ >
+
+
+ {ext.label || 'parsed invoice'}
+
+ {
+ e.stopPropagation();
+ this.handleRemoveExtraction(ext.id);
+ }}
+ aria-label="Entfernen"
+ sx={{
+ p: 0.2,
+ ml: 0.1,
+ color: 'text.secondary',
+ '&:hover': {
+ color: 'error.main',
+ bgcolor: 'action.hover',
+ },
+ }}
+ >
+
+
+
+ ))}
)}
diff --git a/src/components/manifest/Attachment.jsx b/src/components/manifest/Attachment.jsx
index b00f4ad..e86d310 100644
--- a/src/components/manifest/Attachment.jsx
+++ b/src/components/manifest/Attachment.jsx
@@ -1,7 +1,6 @@
import { PureComponent } from 'react';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
-import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import TextSnippetOutlinedIcon from '@mui/icons-material/TextSnippetOutlined';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
@@ -9,7 +8,6 @@ import ChatBubbleOutlineRoundedIcon from '@mui/icons-material/ChatBubbleOutlineR
import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
import { tileBaseSx, TILE_SIZE } from 'dashgui/tiles';
import SkeleHashImage from '../settings/SkeleHashImage';
-import { enqueueSnackbar } from 'notistack';
import { generateTextThumbnail } from '../../utils/textThumbnail';
import chatAutomat from '../../automats/chatAutomat';
import {
@@ -107,17 +105,12 @@ export class Attachment extends PureComponent {
}
const trimmed = (textContent || '').trim();
- if (!trimmed) {
- enqueueSnackbar('Datei enthält keinen Text zum Erwähnen im Chat.', { variant: 'warning' });
- return;
- }
+ if (!trimmed) return;
- const size = attachment.size || new Blob([trimmed]).size;
chatAutomat.actions.insertExtractionSnippet({
text: trimmed,
- size,
+ label: 'parsed invoice',
});
- enqueueSnackbar('Text im Chat erwähnt', { variant: 'success' });
};
handleDelete = (e) => {
@@ -290,7 +283,6 @@ export class Attachment extends PureComponent {
disabled={disabled}
onClick={this.handleRescan}
aria-label="Zu Text"
- title="Zu Text"
sx={{
bgcolor: SCRIM_ACTION,
color: WHITE,
@@ -311,7 +303,6 @@ export class Attachment extends PureComponent {
disabled={disabled}
onClick={this.handleMentionInChat}
aria-label="Im Chat erwähnen"
- title="Im Chat erwähnen"
sx={{
bgcolor: SCRIM_ACTION,
color: WHITE,
@@ -331,7 +322,6 @@ export class Attachment extends PureComponent {
disabled={disabled}
onClick={this.handleDelete}
aria-label="Löschen"
- title="Löschen"
sx={{
bgcolor: SCRIM_ACTION,
color: WHITE,
diff --git a/src/components/settings/BackupSettingsPanel.jsx b/src/components/settings/BackupSettingsPanel.jsx
index 15c615e..5c1aa5a 100644
--- a/src/components/settings/BackupSettingsPanel.jsx
+++ b/src/components/settings/BackupSettingsPanel.jsx
@@ -21,19 +21,28 @@ import TableRow from '@mui/material/TableRow';
import Tabs from '@mui/material/Tabs';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
+import Dialog from '@mui/material/Dialog';
+import DialogTitle from '@mui/material/DialogTitle';
+import DialogContent from '@mui/material/DialogContent';
+import DialogContentText from '@mui/material/DialogContentText';
+import DialogActions from '@mui/material/DialogActions';
import BackupOutlinedIcon from '@mui/icons-material/BackupOutlined';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutlined';
+import CloseIcon from '@mui/icons-material/Close';
import CloudQueueOutlinedIcon from '@mui/icons-material/CloudQueueOutlined';
import FolderOutlinedIcon from '@mui/icons-material/FolderOutlined';
import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
+import PsychologyOutlinedIcon from '@mui/icons-material/PsychologyOutlined';
import RefreshIcon from '@mui/icons-material/Refresh';
import SearchIcon from '@mui/icons-material/Search';
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
import StorageIcon from '@mui/icons-material/Storage';
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
+import WidgetsOutlinedIcon from '@mui/icons-material/WidgetsOutlined';
import { enqueueSnackbar } from 'notistack';
+import { vfs } from '../../api';
import backupAutomat from '../../automats/backupAutomat';
import { notifyError } from '../../utils/notify';
import {
@@ -92,11 +101,18 @@ export default class BackupSettingsPanel extends Component {
vfsLoadingByBackup: vfsLoadingByBackup || {},
tierTab: 'hourly', // 'hourly' | 'daily' | 'monthly'
expandedItemName: null,
+ restoringSkills: false,
+ restoringApplets: false,
+ confirmRestore: null, // null | 'skills' | 'applets'
+ lastRestoreResult: null, // null | { type: 'skills' | 'applets', count: number, restored: string[] }
};
this.handleRefresh = this.handleRefresh.bind(this);
this.handleTierChange = this.handleTierChange.bind(this);
this.handleToggleExpand = this.handleToggleExpand.bind(this);
+ this.handleOpenConfirm = this.handleOpenConfirm.bind(this);
+ this.handleCloseConfirm = this.handleCloseConfirm.bind(this);
+ this.handleExecuteRestore = this.handleExecuteRestore.bind(this);
}
componentDidMount() {
@@ -151,6 +167,65 @@ export default class BackupSettingsPanel extends Component {
}));
}
+ handleOpenConfirm(type) {
+ this.setState({ confirmRestore: type });
+ }
+
+ handleCloseConfirm() {
+ if (this.state.restoringSkills || this.state.restoringApplets) return;
+ this.setState({ confirmRestore: null });
+ }
+
+ // #region handleExecuteRestore
+ // MARK: calls POST /api/vfs/skills/restore or /api/vfs/applets/restore
+ async handleExecuteRestore() {
+ const { confirmRestore } = this.state;
+ if (!confirmRestore) return;
+
+ if (confirmRestore === 'skills') {
+ this.setState({ restoringSkills: true });
+ try {
+ const res = await vfs.restoreSkills();
+ enqueueSnackbar(`${res.count} Standard-Skills (.SKILL) erfolgreich wiederhergestellt.`, {
+ variant: 'success',
+ });
+ this.setState({
+ confirmRestore: null,
+ restoringSkills: false,
+ lastRestoreResult: {
+ type: 'skills',
+ count: res.count,
+ restored: res.restored || [],
+ },
+ });
+ } catch (err) {
+ this.setState({ restoringSkills: false });
+ notifyError(err);
+ }
+ } else if (confirmRestore === 'applets') {
+ this.setState({ restoringApplets: true });
+ try {
+ const res = await vfs.restoreApplets();
+ enqueueSnackbar(`${res.count} Beispiel-Applets (.applet.js) erfolgreich wiederhergestellt.`, {
+ variant: 'success',
+ });
+ this.setState({
+ confirmRestore: null,
+ restoringApplets: false,
+ lastRestoreResult: {
+ type: 'applets',
+ count: res.count,
+ restored: res.restored || [],
+ },
+ });
+ } catch (err) {
+ this.setState({ restoringApplets: false });
+ notifyError(err);
+ }
+ }
+ }
+ // #endregion
+
render() {
const {
status,
@@ -161,6 +236,10 @@ export default class BackupSettingsPanel extends Component {
vfsLoadingByBackup,
tierTab,
expandedItemName,
+ restoringSkills,
+ restoringApplets,
+ confirmRestore,
+ lastRestoreResult,
} = this.state;
if (status === 'loading' && !backupStatus) {
@@ -224,7 +303,7 @@ export default class BackupSettingsPanel extends Component {
size="small"
startIcon={}
onClick={this.handleRefresh}
- disabled={isRunning}
+ disabled={status === 'loading'}
>
Aktualisieren
@@ -246,19 +325,11 @@ export default class BackupSettingsPanel extends Component {
>
: }
- label={backupStatus?.enabled ? 'Dienst: Aktiv' : 'Dienst: Deaktiviert'}
- color={backupStatus?.enabled ? 'success' : 'default'}
+ icon={backupStatus?.enabled && isRunning ? : }
+ label={backupStatus?.enabled && isRunning ? 'Dienst: Aktiv' : 'Dienst: Deaktiviert'}
+ color={backupStatus?.enabled && isRunning ? 'success' : 'default'}
variant="outlined"
/>
- {isRunning && (
- }
- label="Backup wird ausgeführt..."
- color="primary"
- />
- )}
}
@@ -431,6 +502,231 @@ export default class BackupSettingsPanel extends Component {
)}
+
+ {/* System Defaults Restore Section (VFS Skills & Applets) */}
+
+
+
+
+
+ System-Standarddateien wiederherstellen (VFS)
+
+
+
+ Setzt systemweite Vorlagen im virtuellen Dateisystem (VFS) des Unternehmens auf den Auslieferungszustand zurück. Bereits vorhandene gleichnamige Dateien werden dabei überschrieben.
+
+
+
+
+
+ {/* Skills Restore Card */}
+
+
+
+
+
+
+
+
+ Standard-Skills
+
+
+
+
+
+ Stellt alle systemweiten Standard-.SKILL-Dateien für KI-Assistenten und Workflows im VFS wieder her.
+
+
+
+
+
+ Überschreibt existierende Dateien
+
+ : }
+ onClick={() => this.handleOpenConfirm('skills')}
+ disabled={restoringSkills}
+ sx={{ textTransform: 'none', minHeight: 30, fontSize: '0.8rem', fontWeight: 600 }}
+ >
+ Skills wiederherstellen
+
+
+
+
+ {/* Applets Restore Card */}
+
+
+
+
+
+
+
+
+ Beispiel-Applets
+
+
+
+
+
+ Stellt alle systemweiten Standard-.applet.js-Beispiel-Applets für Workflows und Auswertungen im VFS wieder her.
+
+
+
+
+
+ Überschreibt existierende Dateien
+
+ : }
+ onClick={() => this.handleOpenConfirm('applets')}
+ disabled={restoringApplets}
+ sx={{ textTransform: 'none', minHeight: 30, fontSize: '0.8rem', fontWeight: 600 }}
+ >
+ Applets wiederherstellen
+
+
+
+
+
+ {/* Last restore results notification / chip display */}
+ {lastRestoreResult && (
+
+
+
+ Erfolgreich wiederhergestellt ({lastRestoreResult.count} {lastRestoreResult.type === 'skills' ? 'Skills' : 'Applets'}):
+
+ this.setState({ lastRestoreResult: null })}
+ sx={{ p: 0.25, color: 'text.secondary' }}
+ >
+
+
+
+
+ {lastRestoreResult.restored.map((name) => (
+
+ ))}
+
+
+ )}
+
+
+
+ {/* Confirmation Dialog for System Defaults Restore */}
+
);
}
diff --git a/src/routes/lieferung/neu.jsx b/src/routes/lieferung/neu.jsx
index 573a3b9..4e23548 100644
--- a/src/routes/lieferung/neu.jsx
+++ b/src/routes/lieferung/neu.jsx
@@ -659,20 +659,17 @@ class LieferungNeuPage extends PureComponent {
const existingTxt = this.state.attachments?.find(
(a) => a.parentPdfId === attachment.id || a.name === txtName,
);
- let targetTxt = existingTxt;
if (existingTxt) {
- const updated = {
+ this.automat.actions.updateAttachment({
id: existingTxt.id,
file: txtFile,
size: txtFile.size,
text,
thumbnail: txtThumbnail || existingTxt.thumbnail,
isDirty: true,
- };
- this.automat.actions.updateAttachment(updated);
- targetTxt = { ...existingTxt, ...updated };
+ });
} else {
- targetTxt = {
+ this.automat.actions.addAttachment({
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
file: txtFile,
name: txtFile.name,
@@ -682,14 +679,10 @@ class LieferungNeuPage extends PureComponent {
text,
parentPdfId: attachment.id,
uploadedAt: new Date().toISOString(),
- };
- this.automat.actions.addAttachment(targetTxt);
+ });
}
- this.handleAttachmentClick(targetTxt);
- enqueueSnackbar('Text aus PDF extrahiert und geöffnet', { variant: 'success' });
} catch (txtErr) {
console.error('Fehler beim Aktualisieren der Textdatei:', txtErr);
- enqueueSnackbar('Fehler beim Extrahieren von Text aus der PDF', { variant: 'error' });
}
}
}
@@ -715,17 +708,12 @@ class LieferungNeuPage extends PureComponent {
}
const trimmed = (text || '').trim();
- if (!trimmed) {
- enqueueSnackbar('Datei enthält keinen Text zum Erwähnen im Chat.', { variant: 'warning' });
- return;
- }
+ if (!trimmed) return;
- const size = attachment.size || new Blob([trimmed]).size;
chatAutomat.actions.insertExtractionSnippet({
text: trimmed,
- size,
+ label: 'parsed invoice',
});
- enqueueSnackbar('Text im Chat erwähnt', { variant: 'success' });
}
handlePdfDelete(attachment) {
diff --git a/test/dummyBackend.mjs b/test/dummyBackend.mjs
index 66ce651..61c3b19 100644
--- a/test/dummyBackend.mjs
+++ b/test/dummyBackend.mjs
@@ -505,6 +505,12 @@ const server = http.createServer(async (req, res) => {
if (p === '/api/vfs/upload' && method === 'POST') {
return json(res, 200, { files: [] });
}
+ if (p === '/api/vfs/skills/restore' && method === 'POST') {
+ return json(res, 200, { success: true, restored: ['system.SKILL'], count: 1 });
+ }
+ if (p === '/api/vfs/applets/restore' && method === 'POST') {
+ return json(res, 200, { success: true, restored: ['example.applet.js'], count: 1 });
+ }
if (p.startsWith('/api/vfs/') && method === 'PUT') {
return json(res, 200, { ok: true });
}