openwriter 0.5.5 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/pad.js +3 -0
- package/dist/client/assets/index-CCMCrgTu.js +209 -0
- package/dist/client/assets/index-DN1_4Au6.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/connection-routes.js +327 -0
- package/dist/server/connections.js +35 -0
- package/dist/server/documents.js +30 -24
- package/dist/server/export-html-template.js +1 -1
- package/dist/server/export-routes.js +1 -1
- package/dist/server/gdoc-import.js +2 -2
- package/dist/server/git-sync.js +30 -30
- package/dist/server/helpers.js +175 -20
- package/dist/server/image-upload.js +10 -7
- package/dist/server/index.js +162 -5
- package/dist/server/install-skill.js +119 -2
- package/dist/server/marks.js +11 -11
- package/dist/server/mcp.js +90 -16
- package/dist/server/plugin-manager.js +2 -2
- package/dist/server/scheduler-routes.js +121 -0
- package/dist/server/state.js +126 -42
- package/dist/server/versions.js +6 -2
- package/dist/server/workspaces.js +7 -7
- package/dist/server/ws.js +15 -0
- package/package.json +1 -1
- package/skill/SKILL.md +106 -25
- package/dist/client/assets/index-97-SDxVw.js +0 -210
- package/dist/client/assets/index-BR_sMmFf.css +0 -1
package/dist/server/marks.js
CHANGED
|
@@ -5,17 +5,17 @@
|
|
|
5
5
|
import { join } from 'path';
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, unlinkSync, renameSync } from 'fs';
|
|
7
7
|
import { randomUUID } from 'crypto';
|
|
8
|
-
import {
|
|
9
|
-
|
|
8
|
+
import { getDataDir, ensureDataDir } from './helpers.js';
|
|
9
|
+
function getMarksDir() { return join(getDataDir(), '_marks'); }
|
|
10
10
|
function ensureMarksDir() {
|
|
11
11
|
ensureDataDir();
|
|
12
|
-
if (!existsSync(
|
|
13
|
-
mkdirSync(
|
|
12
|
+
if (!existsSync(getMarksDir()))
|
|
13
|
+
mkdirSync(getMarksDir(), { recursive: true });
|
|
14
14
|
}
|
|
15
15
|
function markFilePath(filename) {
|
|
16
16
|
// Sanitize: replace path separators to avoid nested paths
|
|
17
17
|
const safe = filename.replace(/[/\\]/g, '_');
|
|
18
|
-
return join(
|
|
18
|
+
return join(getMarksDir(), `${safe}.json`);
|
|
19
19
|
}
|
|
20
20
|
function readMarkFile(filename) {
|
|
21
21
|
const path = markFilePath(filename);
|
|
@@ -63,13 +63,13 @@ export function getMarks(filename) {
|
|
|
63
63
|
ensureMarksDir();
|
|
64
64
|
const result = {};
|
|
65
65
|
try {
|
|
66
|
-
const files = readdirSync(
|
|
66
|
+
const files = readdirSync(getMarksDir());
|
|
67
67
|
for (const file of files) {
|
|
68
68
|
if (!file.endsWith('.json'))
|
|
69
69
|
continue;
|
|
70
70
|
const docFilename = file.replace(/\.json$/, '').replace(/_/g, ' ');
|
|
71
71
|
// Read raw to avoid filename roundtrip issues
|
|
72
|
-
const path = join(
|
|
72
|
+
const path = join(getMarksDir(), file);
|
|
73
73
|
try {
|
|
74
74
|
const data = JSON.parse(readFileSync(path, 'utf-8'));
|
|
75
75
|
if (data.marks.length > 0)
|
|
@@ -90,7 +90,7 @@ export function getGlobalMarkSummary(excludeFilename) {
|
|
|
90
90
|
let totalMarks = 0;
|
|
91
91
|
let docCount = 0;
|
|
92
92
|
try {
|
|
93
|
-
const files = readdirSync(
|
|
93
|
+
const files = readdirSync(getMarksDir());
|
|
94
94
|
for (const file of files) {
|
|
95
95
|
if (!file.endsWith('.json'))
|
|
96
96
|
continue;
|
|
@@ -99,7 +99,7 @@ export function getGlobalMarkSummary(excludeFilename) {
|
|
|
99
99
|
if (file === `${safe}.json`)
|
|
100
100
|
continue;
|
|
101
101
|
}
|
|
102
|
-
const path = join(
|
|
102
|
+
const path = join(getMarksDir(), file);
|
|
103
103
|
try {
|
|
104
104
|
const data = JSON.parse(readFileSync(path, 'utf-8'));
|
|
105
105
|
if (data.marks.length > 0) {
|
|
@@ -118,11 +118,11 @@ export function resolveMarks(ids) {
|
|
|
118
118
|
const resolved = [];
|
|
119
119
|
ensureMarksDir();
|
|
120
120
|
try {
|
|
121
|
-
const files = readdirSync(
|
|
121
|
+
const files = readdirSync(getMarksDir());
|
|
122
122
|
for (const file of files) {
|
|
123
123
|
if (!file.endsWith('.json'))
|
|
124
124
|
continue;
|
|
125
|
-
const filePath = join(
|
|
125
|
+
const filePath = join(getMarksDir(), file);
|
|
126
126
|
try {
|
|
127
127
|
const data = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
128
128
|
const before = data.marks.length;
|
package/dist/server/mcp.js
CHANGED
|
@@ -9,7 +9,7 @@ import { randomUUID } from 'crypto';
|
|
|
9
9
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
10
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
11
11
|
import { z } from 'zod';
|
|
12
|
-
import {
|
|
12
|
+
import { getDataDir, ensureDataDir, resolveDocPath } from './helpers.js';
|
|
13
13
|
import { getDocument, getWordCount, getPendingChangeCount, getTitle, getStatus, getNodesByIds, getMetadata, setMetadata, applyChanges, applyTextEdits, updateDocument, save, markAllNodesAsPending, setAgentLock, updatePendingCacheForActiveDoc, populateDocumentFile, applyChangesToFile, applyTextEditsToFile, getDocId, getFilePath, } from './state.js';
|
|
14
14
|
import { listDocuments, switchDocument, createDocument, createDocumentFile, deleteDocument, openFile, getActiveFilename, updateDocumentTitle, promoteTempFile, archiveDocument, unarchiveDocument, resolveDocId } from './documents.js';
|
|
15
15
|
import { broadcastDocumentSwitched, broadcastDocumentsChanged, broadcastWorkspacesChanged, broadcastTitleChanged, broadcastMetadataChanged, broadcastPendingDocsChanged, broadcastWritingStarted, broadcastWritingFinished } from './ws.js';
|
|
@@ -24,6 +24,19 @@ import { listVersions, forceSnapshot, restoreVersion } from './versions.js';
|
|
|
24
24
|
import { markdownToTiptap } from './markdown.js';
|
|
25
25
|
import { getMarks, getMarkCount, getGlobalMarkSummary, resolveMarks } from './marks.js';
|
|
26
26
|
import { broadcastMarksChanged } from './ws.js';
|
|
27
|
+
/** Map a content type string to its frontmatter metadata object. */
|
|
28
|
+
function resolveTypeMeta(type) {
|
|
29
|
+
switch (type) {
|
|
30
|
+
case 'tweet': return { tweetContext: { mode: 'tweet' } };
|
|
31
|
+
case 'reply': return { tweetContext: { mode: 'reply' } };
|
|
32
|
+
case 'quote': return { tweetContext: { mode: 'quote' } };
|
|
33
|
+
case 'article': return { articleContext: { active: true } };
|
|
34
|
+
case 'linkedin': return { linkedinContext: { active: true } };
|
|
35
|
+
case 'newsletter': return { newsletterContext: { active: true } };
|
|
36
|
+
case 'blog': return { blogContext: { active: true } };
|
|
37
|
+
default: return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
27
40
|
/** Check if a document is in tweet compose mode (has tweetContext metadata). */
|
|
28
41
|
function isTweetDoc(filename) {
|
|
29
42
|
if (!filename || filename === getActiveFilename()) {
|
|
@@ -172,15 +185,24 @@ export const TOOL_REGISTRY = [
|
|
|
172
185
|
},
|
|
173
186
|
{
|
|
174
187
|
name: 'create_document',
|
|
175
|
-
description: 'Create a new empty document and switch to it. Always provide a title. Saves the current document first. By default shows a sidebar spinner that persists until populate_document is called — set empty=true to skip the spinner and switch immediately (use for template docs like tweets/articles that don\'t need agent content). If workspace is provided, the doc is automatically added to it (workspace is created if it doesn\'t exist). If container is also provided, the doc is placed inside that container (created if it doesn\'t exist).',
|
|
188
|
+
description: 'Create a new empty document and switch to it. Always provide a title. Saves the current document first. By default shows a sidebar spinner that persists until populate_document is called — set empty=true to skip the spinner and switch immediately (use for template docs like tweets/articles that don\'t need agent content). If workspace is provided, the doc is automatically added to it (workspace is created if it doesn\'t exist). If container is also provided, the doc is placed inside that container (created if it doesn\'t exist). Use content_type to create typed documents (tweet, article, linkedin, etc.) with the correct metadata pre-set.',
|
|
176
189
|
schema: {
|
|
177
190
|
title: z.string().optional().describe('Title for the new document. Defaults to "Untitled".'),
|
|
178
191
|
path: z.string().optional().describe('Absolute file path to create the document at (e.g. "C:/projects/doc.md"). If omitted, creates in ~/.openwriter/.'),
|
|
179
192
|
workspace: z.string().optional().describe('Workspace title to add this doc to. Creates the workspace if it doesn\'t exist.'),
|
|
180
193
|
container: z.string().optional().describe('Container name within the workspace (e.g. "Chapters", "Notes", "References"). Creates the container if it doesn\'t exist. Requires workspace.'),
|
|
181
194
|
empty: z.boolean().optional().describe('If true, skip the writing spinner and switch to the doc immediately. No need to call populate_document. Use for template docs (tweets, articles) that start empty.'),
|
|
182
|
-
|
|
183
|
-
|
|
195
|
+
content_type: z.string().optional().describe('Content type: tweet, reply, quote, article, linkedin, newsletter, or blog. Sets metadata so the doc is recognized as that type. For reply/quote, use set_metadata after creation to set the target tweet URL.'),
|
|
196
|
+
},
|
|
197
|
+
handler: async ({ title, path, workspace, container, empty, content_type }) => {
|
|
198
|
+
// Default title from content_type if not provided
|
|
199
|
+
if (!title && content_type) {
|
|
200
|
+
const typeDefaults = {
|
|
201
|
+
tweet: 'Tweet', reply: 'Reply', quote: 'Quote Tweet', article: 'Article',
|
|
202
|
+
linkedin: 'LinkedIn Post', newsletter: 'Newsletter', blog: 'Blog Post',
|
|
203
|
+
};
|
|
204
|
+
title = typeDefaults[content_type];
|
|
205
|
+
}
|
|
184
206
|
// Resolve workspace/container up front so spinner renders in the right place
|
|
185
207
|
let wsTarget;
|
|
186
208
|
if (workspace) {
|
|
@@ -203,6 +225,13 @@ export const TOOL_REGISTRY = [
|
|
|
203
225
|
// Immediate switch — no spinner, no populate_document needed
|
|
204
226
|
setAgentLock();
|
|
205
227
|
const result = createDocument(title, undefined, path);
|
|
228
|
+
// Apply type-specific metadata
|
|
229
|
+
if (content_type) {
|
|
230
|
+
const typeMeta = resolveTypeMeta(content_type);
|
|
231
|
+
if (typeMeta) {
|
|
232
|
+
setMetadata(typeMeta);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
206
235
|
let wsInfo = '';
|
|
207
236
|
if (wsTarget) {
|
|
208
237
|
addDoc(wsTarget.wsFilename, wsTarget.containerId, result.filename, result.title);
|
|
@@ -212,17 +241,18 @@ export const TOOL_REGISTRY = [
|
|
|
212
241
|
save();
|
|
213
242
|
broadcastDocumentsChanged();
|
|
214
243
|
broadcastWorkspacesChanged();
|
|
215
|
-
broadcastDocumentSwitched(getDocument(), getTitle(), getActiveFilename());
|
|
244
|
+
broadcastDocumentSwitched(getDocument(), getTitle(), getActiveFilename(), getMetadata());
|
|
216
245
|
return {
|
|
217
246
|
content: [{
|
|
218
247
|
type: 'text',
|
|
219
|
-
text: `Created "${result.title}" [${newDocId}]${wsInfo} — ready.`,
|
|
248
|
+
text: `Created "${result.title}" [${newDocId}]${wsInfo}${content_type ? ` (${content_type})` : ''} — ready.`,
|
|
220
249
|
}],
|
|
221
250
|
};
|
|
222
251
|
}
|
|
223
252
|
// Two-step flow: create file on disk WITHOUT switching the user's view.
|
|
224
253
|
// The spinner persists in the sidebar until populate_document is called.
|
|
225
|
-
const
|
|
254
|
+
const typeMeta = content_type ? resolveTypeMeta(content_type) : undefined;
|
|
255
|
+
const result = createDocumentFile(title, path, typeMeta);
|
|
226
256
|
let wsInfo = '';
|
|
227
257
|
if (wsTarget) {
|
|
228
258
|
addDoc(wsTarget.wsFilename, wsTarget.containerId, result.filename, result.title);
|
|
@@ -736,7 +766,7 @@ export const TOOL_REGISTRY = [
|
|
|
736
766
|
}
|
|
737
767
|
// Save to ~/.openwriter/_images/
|
|
738
768
|
ensureDataDir();
|
|
739
|
-
const imagesDir = join(
|
|
769
|
+
const imagesDir = join(getDataDir(), '_images');
|
|
740
770
|
if (!existsSync(imagesDir))
|
|
741
771
|
mkdirSync(imagesDir, { recursive: true });
|
|
742
772
|
const filename = `${randomUUID().slice(0, 8)}.png`;
|
|
@@ -756,8 +786,10 @@ export const TOOL_REGISTRY = [
|
|
|
756
786
|
}],
|
|
757
787
|
};
|
|
758
788
|
}
|
|
759
|
-
// Use
|
|
760
|
-
|
|
789
|
+
// Use LIVE metadata for coverImages (not stale pre-await snapshot)
|
|
790
|
+
// so concurrent generate_image calls don't overwrite each other's results
|
|
791
|
+
const liveMeta = getMetadata();
|
|
792
|
+
const articleContext = liveMeta.articleContext || {};
|
|
761
793
|
let existing = Array.isArray(articleContext.coverImages) ? [...articleContext.coverImages] : [];
|
|
762
794
|
// Seed with current coverImage if array is empty (first carousel entry)
|
|
763
795
|
if (existing.length === 0 && articleContext.coverImage) {
|
|
@@ -811,7 +843,7 @@ export const TOOL_REGISTRY = [
|
|
|
811
843
|
}
|
|
812
844
|
// Save to ~/.openwriter/_images/
|
|
813
845
|
ensureDataDir();
|
|
814
|
-
const imagesDir = join(
|
|
846
|
+
const imagesDir = join(getDataDir(), '_images');
|
|
815
847
|
if (!existsSync(imagesDir))
|
|
816
848
|
mkdirSync(imagesDir, { recursive: true });
|
|
817
849
|
const imgFilename = `${randomUUID().slice(0, 8)}.png`;
|
|
@@ -960,21 +992,59 @@ export const TOOL_REGISTRY = [
|
|
|
960
992
|
},
|
|
961
993
|
},
|
|
962
994
|
];
|
|
963
|
-
/**
|
|
995
|
+
/** Live MCP server instance — used to register plugin tools dynamically. */
|
|
996
|
+
let mcpServerInstance = null;
|
|
997
|
+
/** Convert a JSON Schema properties object to a Zod shape for MCP tool registration. */
|
|
998
|
+
function jsonSchemaToZodShape(inputSchema) {
|
|
999
|
+
const properties = (inputSchema.properties || {});
|
|
1000
|
+
const required = new Set((inputSchema.required || []));
|
|
1001
|
+
const shape = {};
|
|
1002
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
1003
|
+
let field;
|
|
1004
|
+
switch (prop.type) {
|
|
1005
|
+
case 'number':
|
|
1006
|
+
field = z.number();
|
|
1007
|
+
break;
|
|
1008
|
+
case 'boolean':
|
|
1009
|
+
field = z.boolean();
|
|
1010
|
+
break;
|
|
1011
|
+
default:
|
|
1012
|
+
field = z.string();
|
|
1013
|
+
break;
|
|
1014
|
+
}
|
|
1015
|
+
if (prop.description)
|
|
1016
|
+
field = field.describe(prop.description);
|
|
1017
|
+
if (!required.has(key))
|
|
1018
|
+
field = field.optional();
|
|
1019
|
+
shape[key] = field;
|
|
1020
|
+
}
|
|
1021
|
+
return shape;
|
|
1022
|
+
}
|
|
1023
|
+
/** Register MCP tools from plugins. Dynamically adds to the live MCP session. */
|
|
964
1024
|
export function registerPluginTools(tools) {
|
|
965
1025
|
for (const tool of tools) {
|
|
966
|
-
|
|
1026
|
+
const zodShape = jsonSchemaToZodShape(tool.inputSchema);
|
|
1027
|
+
const toolDef = {
|
|
967
1028
|
name: tool.name,
|
|
968
1029
|
description: tool.description,
|
|
969
|
-
schema:
|
|
1030
|
+
schema: zodShape,
|
|
970
1031
|
handler: async (args) => {
|
|
971
1032
|
const result = await tool.handler(args);
|
|
972
1033
|
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
973
1034
|
},
|
|
974
|
-
}
|
|
1035
|
+
};
|
|
1036
|
+
TOOL_REGISTRY.push(toolDef);
|
|
1037
|
+
// Register on live MCP server so existing sessions see it immediately
|
|
1038
|
+
if (mcpServerInstance) {
|
|
1039
|
+
mcpServerInstance.tool(tool.name, tool.description, zodShape, toolDef.handler);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
// Notify connected clients that the tool list changed
|
|
1043
|
+
if (mcpServerInstance) {
|
|
1044
|
+
mcpServerInstance.server.sendToolListChanged().catch(() => { });
|
|
975
1045
|
}
|
|
976
1046
|
}
|
|
977
|
-
/** Remove MCP tools by name.
|
|
1047
|
+
/** Remove MCP tools by name. Notifies connected clients of the change. */
|
|
978
1048
|
export function removePluginTools(names) {
|
|
979
1049
|
const nameSet = new Set(names);
|
|
980
1050
|
for (let i = TOOL_REGISTRY.length - 1; i >= 0; i--) {
|
|
@@ -982,6 +1052,9 @@ export function removePluginTools(names) {
|
|
|
982
1052
|
TOOL_REGISTRY.splice(i, 1);
|
|
983
1053
|
}
|
|
984
1054
|
}
|
|
1055
|
+
if (mcpServerInstance) {
|
|
1056
|
+
mcpServerInstance.server.sendToolListChanged().catch(() => { });
|
|
1057
|
+
}
|
|
985
1058
|
}
|
|
986
1059
|
export async function startMcpServer() {
|
|
987
1060
|
const server = new McpServer({
|
|
@@ -991,6 +1064,7 @@ export async function startMcpServer() {
|
|
|
991
1064
|
for (const tool of TOOL_REGISTRY) {
|
|
992
1065
|
server.tool(tool.name, tool.description, tool.schema, tool.handler);
|
|
993
1066
|
}
|
|
1067
|
+
mcpServerInstance = server;
|
|
994
1068
|
const transport = new StdioServerTransport();
|
|
995
1069
|
await server.connect(transport);
|
|
996
1070
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { Router as createRouter } from 'express';
|
|
6
6
|
import { discoverPlugins, loadPluginModule } from './plugin-discovery.js';
|
|
7
7
|
import { registerPluginTools, removePluginTools } from './mcp.js';
|
|
8
|
-
import { readConfig, saveConfig } from './helpers.js';
|
|
8
|
+
import { readConfig, saveConfig, getDataDir } from './helpers.js';
|
|
9
9
|
import { broadcastPluginsChanged } from './ws.js';
|
|
10
10
|
export class PluginManager {
|
|
11
11
|
app;
|
|
@@ -55,7 +55,7 @@ export class PluginManager {
|
|
|
55
55
|
// Register routes via togglable middleware
|
|
56
56
|
if (plugin.registerRoutes) {
|
|
57
57
|
const router = createRouter();
|
|
58
|
-
await plugin.registerRoutes({ app: router, config: resolvedConfig });
|
|
58
|
+
await plugin.registerRoutes({ app: router, config: resolvedConfig, dataDir: getDataDir() });
|
|
59
59
|
managed.router = router;
|
|
60
60
|
// Wrap in middleware that skips when disabled
|
|
61
61
|
managed.middleware = (req, res, next) => {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler routes — proxy all requests to the platform API.
|
|
3
|
+
*/
|
|
4
|
+
import { Router } from 'express';
|
|
5
|
+
import { platformFetch, isAuthenticated } from './connections.js';
|
|
6
|
+
export function createSchedulerRouter() {
|
|
7
|
+
const router = Router();
|
|
8
|
+
function proxy(path, method = 'GET') {
|
|
9
|
+
return async (req, res) => {
|
|
10
|
+
try {
|
|
11
|
+
if (!isAuthenticated()) {
|
|
12
|
+
res.json({ error: 'Not authenticated' });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const options = { method };
|
|
16
|
+
if (method !== 'GET' && method !== 'DELETE') {
|
|
17
|
+
options.body = JSON.stringify(req.body);
|
|
18
|
+
}
|
|
19
|
+
const upstream = await platformFetch(path, options);
|
|
20
|
+
const data = await upstream.json();
|
|
21
|
+
if (!upstream.ok) {
|
|
22
|
+
res.status(upstream.status).json(data);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
res.json(data);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
res.status(500).json({ error: err.message });
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// Slots
|
|
33
|
+
router.get('/api/scheduler/slots', proxy('/scheduler/slots'));
|
|
34
|
+
router.post('/api/scheduler/slots', proxy('/scheduler/slots', 'POST'));
|
|
35
|
+
router.patch('/api/scheduler/slots/:id', async (req, res) => {
|
|
36
|
+
try {
|
|
37
|
+
if (!isAuthenticated()) {
|
|
38
|
+
res.json({ error: 'Not authenticated' });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const upstream = await platformFetch(`/scheduler/slots/${req.params.id}`, {
|
|
42
|
+
method: 'PATCH',
|
|
43
|
+
body: JSON.stringify(req.body),
|
|
44
|
+
});
|
|
45
|
+
const data = await upstream.json();
|
|
46
|
+
if (!upstream.ok) {
|
|
47
|
+
res.status(upstream.status).json(data);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
res.json(data);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
res.status(500).json({ error: err.message });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
router.delete('/api/scheduler/slots/:id', async (req, res) => {
|
|
57
|
+
try {
|
|
58
|
+
if (!isAuthenticated()) {
|
|
59
|
+
res.json({ error: 'Not authenticated' });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const upstream = await platformFetch(`/scheduler/slots/${req.params.id}`, { method: 'DELETE' });
|
|
63
|
+
const data = await upstream.json();
|
|
64
|
+
if (!upstream.ok) {
|
|
65
|
+
res.status(upstream.status).json(data);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
res.json(data);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
res.status(500).json({ error: err.message });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
// Queue
|
|
75
|
+
router.get('/api/scheduler/queue', proxy('/scheduler/queue'));
|
|
76
|
+
router.post('/api/scheduler/queue', proxy('/scheduler/queue', 'POST'));
|
|
77
|
+
router.patch('/api/scheduler/queue/:id', async (req, res) => {
|
|
78
|
+
try {
|
|
79
|
+
if (!isAuthenticated()) {
|
|
80
|
+
res.json({ error: 'Not authenticated' });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const upstream = await platformFetch(`/scheduler/queue/${req.params.id}`, {
|
|
84
|
+
method: 'PATCH',
|
|
85
|
+
body: JSON.stringify(req.body),
|
|
86
|
+
});
|
|
87
|
+
const data = await upstream.json();
|
|
88
|
+
if (!upstream.ok) {
|
|
89
|
+
res.status(upstream.status).json(data);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
res.json(data);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
res.status(500).json({ error: err.message });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
router.delete('/api/scheduler/queue/:id', async (req, res) => {
|
|
99
|
+
try {
|
|
100
|
+
if (!isAuthenticated()) {
|
|
101
|
+
res.json({ error: 'Not authenticated' });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const upstream = await platformFetch(`/scheduler/queue/${req.params.id}`, { method: 'DELETE' });
|
|
105
|
+
const data = await upstream.json();
|
|
106
|
+
if (!upstream.ok) {
|
|
107
|
+
res.status(upstream.status).json(data);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
res.json(data);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
res.status(500).json({ error: err.message });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
// History
|
|
117
|
+
router.get('/api/scheduler/history', proxy('/scheduler/history'));
|
|
118
|
+
// Available connections for scheduler
|
|
119
|
+
router.get('/api/scheduler/connections', proxy('/scheduler/connections'));
|
|
120
|
+
return router;
|
|
121
|
+
}
|