added nextcloud
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
{
|
||||
"openapi": "3.0.1",
|
||||
"info": {
|
||||
"title": "Scheduler API",
|
||||
"description": "API for managing scheduled prompts. Cron expressions run in America/New_York (EST).",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "/",
|
||||
"description": "Current server"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/api/schedules": {
|
||||
"get": {
|
||||
"summary": "List schedules",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Schedule"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"summary": "Create or replace a schedule",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ScheduleInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Schedule created or updated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/components/responses/BadRequest"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/schedules/{name}": {
|
||||
"delete": {
|
||||
"summary": "Delete a schedule",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "Schedule name (raw or scoped)",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Schedule deleted"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#/components/responses/Forbidden"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/components/responses/NotFound"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"BearerAuth": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "JWT"
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"Schedule": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Scoped identifier used internally"
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"userId": {
|
||||
"type": "string"
|
||||
},
|
||||
"schedules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "Cron expression"
|
||||
}
|
||||
},
|
||||
"startAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"nullable": true
|
||||
},
|
||||
"oneShot": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"templateRef": {
|
||||
"$ref": "#/components/schemas/TemplateRef"
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"tools": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fname": {
|
||||
"type": "string"
|
||||
},
|
||||
"fkey": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"userId",
|
||||
"schedules",
|
||||
"oneShot"
|
||||
]
|
||||
},
|
||||
"ScheduleInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"when": {
|
||||
"$ref": "#/components/schemas/ScheduleWhen"
|
||||
},
|
||||
"oneShot": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"template": {
|
||||
"$ref": "#/components/schemas/TemplateInput"
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fname": {
|
||||
"type": "string"
|
||||
},
|
||||
"fkey": {
|
||||
"type": "string",
|
||||
"description": "Optional previously stored file reference"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Base64 encoded file contents"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fname"
|
||||
]
|
||||
}
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"tools": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"when",
|
||||
"template",
|
||||
"parameters",
|
||||
"prompt",
|
||||
"model",
|
||||
"tools"
|
||||
]
|
||||
},
|
||||
"ScheduleWhen": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cron": {
|
||||
"type": "string",
|
||||
"description": "5-field cron expression evaluated in America/New_York"
|
||||
},
|
||||
"start": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Optional start gate; required when oneShot is true"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"TemplateInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"clusterScope": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"TemplateRef": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"clusterScope": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"Unauthorized": {
|
||||
"description": "Missing or invalid credentials"
|
||||
},
|
||||
"BadRequest": {
|
||||
"description": "Invalid request payload",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Forbidden": {
|
||||
"description": "Schedule exists but belongs to another user"
|
||||
},
|
||||
"NotFound": {
|
||||
"description": "Schedule not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-242
@@ -8,242 +8,11 @@ import cron from 'node-cron'
|
||||
import loginUser, { getUser } from './helpers/resolve-user'
|
||||
import { authCall, callNewChat, ollamaInp, schedInp } from './helpers/ollamaCalls'
|
||||
|
||||
const DEFAULT_TZ = "America/New_York"
|
||||
|
||||
const openApiSpec = {
|
||||
openapi: '3.0.1',
|
||||
info: {
|
||||
title: 'Scheduler API',
|
||||
description: 'API for managing scheduled prompts. Cron expressions run in America/New_York (EST).',
|
||||
version: '1.0.0'
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: '/',
|
||||
description: 'Current server'
|
||||
}
|
||||
],
|
||||
paths: {
|
||||
'/api/schedules': {
|
||||
get: {
|
||||
summary: 'List schedules',
|
||||
security: [{ BearerAuth: [] }],
|
||||
responses: {
|
||||
200: {
|
||||
description: 'Successful response',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
ok: { type: 'boolean', example: true },
|
||||
items: {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/Schedule' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
401: { $ref: '#/components/responses/Unauthorized' }
|
||||
}
|
||||
},
|
||||
post: {
|
||||
summary: 'Create or replace a schedule',
|
||||
security: [{ BearerAuth: [] }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/ScheduleInput' }
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
201: {
|
||||
description: 'Schedule created or updated',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
ok: { type: 'boolean', example: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
400: { $ref: '#/components/responses/BadRequest' },
|
||||
401: { $ref: '#/components/responses/Unauthorized' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/schedules/{name}': {
|
||||
delete: {
|
||||
summary: 'Delete a schedule',
|
||||
security: [{ BearerAuth: [] }],
|
||||
parameters: [
|
||||
{
|
||||
name: 'name',
|
||||
in: 'path',
|
||||
description: 'Schedule name (raw or scoped)',
|
||||
required: true,
|
||||
schema: { type: 'string' }
|
||||
}
|
||||
],
|
||||
responses: {
|
||||
204: { description: 'Schedule deleted' },
|
||||
401: { $ref: '#/components/responses/Unauthorized' },
|
||||
403: { $ref: '#/components/responses/Forbidden' },
|
||||
404: { $ref: '#/components/responses/NotFound' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
securitySchemes: {
|
||||
BearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT'
|
||||
}
|
||||
},
|
||||
schemas: {
|
||||
Schedule: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Scoped identifier used internally' },
|
||||
displayName: { type: 'string' },
|
||||
userId: { type: 'string' },
|
||||
schedules: {
|
||||
type: 'array',
|
||||
items: { type: 'string', description: 'Cron expression' }
|
||||
},
|
||||
startAt: { type: 'string', format: 'date-time', nullable: true },
|
||||
oneShot: { type: 'boolean' },
|
||||
templateRef: { $ref: '#/components/schemas/TemplateRef' },
|
||||
prompt: { type: 'string' },
|
||||
model: { type: 'string' },
|
||||
tools: { type: 'array', items: { type: 'string' } },
|
||||
features: {
|
||||
type: 'object',
|
||||
additionalProperties: { type: 'boolean' }
|
||||
},
|
||||
files: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fname: { type: 'string' },
|
||||
fkey: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['name', 'userId', 'schedules', 'oneShot']
|
||||
},
|
||||
ScheduleInput: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
when: { $ref: '#/components/schemas/ScheduleWhen' },
|
||||
oneShot: { type: 'boolean', default: false },
|
||||
template: { $ref: '#/components/schemas/TemplateInput' },
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: true
|
||||
},
|
||||
files: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fname: { type: 'string' },
|
||||
fkey: {
|
||||
type: 'string',
|
||||
description: 'Optional previously stored file reference'
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Base64 encoded file contents'
|
||||
}
|
||||
},
|
||||
required: ['fname']
|
||||
}
|
||||
},
|
||||
prompt: { type: 'string' },
|
||||
model: { type: 'string' },
|
||||
tools: { type: 'array', items: { type: 'string' } },
|
||||
features: {
|
||||
type: 'object',
|
||||
additionalProperties: { type: 'boolean' }
|
||||
}
|
||||
},
|
||||
required: ['name', 'when', 'template', 'parameters', 'prompt', 'model', 'tools']
|
||||
},
|
||||
ScheduleWhen: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
cron: {
|
||||
type: 'string',
|
||||
description: '5-field cron expression evaluated in America/New_York'
|
||||
},
|
||||
start: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
description: 'Optional start gate; required when oneShot is true'
|
||||
}
|
||||
},
|
||||
required: []
|
||||
},
|
||||
TemplateInput: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
clusterScope: { type: 'boolean', default: false }
|
||||
},
|
||||
required: ['name']
|
||||
},
|
||||
TemplateRef: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
clusterScope: { type: 'boolean' }
|
||||
},
|
||||
required: ['name']
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
Unauthorized: {
|
||||
description: 'Missing or invalid credentials'
|
||||
},
|
||||
BadRequest: {
|
||||
description: 'Invalid request payload',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
ok: { type: 'boolean', example: false },
|
||||
error: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Forbidden: {
|
||||
description: 'Schedule exists but belongs to another user'
|
||||
},
|
||||
NotFound: {
|
||||
description: 'Schedule not found'
|
||||
}
|
||||
}
|
||||
}
|
||||
} as const;
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url),
|
||||
// KEEP AS STRING
|
||||
const openApiSpec = fs.readFileSync('./openapi.json', 'utf-8'),
|
||||
__filename = fileURLToPath(import.meta.url),
|
||||
__dirname = path.dirname(__filename),
|
||||
DEFAULT_TZ = "America/New_York",
|
||||
|
||||
// folders/files
|
||||
DATA_DIR = process.env.DATA_DIR || path.join(__dirname, 'data'),
|
||||
@@ -254,8 +23,8 @@ const __filename = fileURLToPath(import.meta.url),
|
||||
|
||||
let FILES_DIR = FILES_DIR_PREFERRED;
|
||||
|
||||
// defaults
|
||||
PORT = Number(process.env.PORT) || 12253
|
||||
// defaults
|
||||
const PORT = Number(process.env.PORT) || 12253
|
||||
|
||||
// connect to docker (via local socket by default)
|
||||
// const docker = new Docker({ socketPath: process.env.DOCKER_SOCKET || '/var/run/docker.sock' })
|
||||
@@ -294,7 +63,6 @@ const normalizeFeatures = (value: unknown): Record<string, boolean> => {
|
||||
type IncomingFile = { fname?: string, fkey?: string, content?: string };
|
||||
|
||||
const sanitizeBase64 = (raw?: string) => typeof raw === 'string' ? raw.replace(/^data:[^;]+;base64,/, '') : '';
|
||||
|
||||
const prepareIncomingFiles = (value: unknown): { fname: string, fkey: string }[] | string => {
|
||||
if (!value) return [];
|
||||
if (!Array.isArray(value)) return 'files must be an array';
|
||||
@@ -302,20 +70,23 @@ const prepareIncomingFiles = (value: unknown): { fname: string, fkey: string }[]
|
||||
const saved: { fname: string, fkey: string }[] = [];
|
||||
for (const entry of value as IncomingFile[]) {
|
||||
if (!entry || typeof entry !== 'object') return 'invalid file entry';
|
||||
|
||||
const fname = typeof entry.fname === 'string' ? entry.fname.trim() : '';
|
||||
if (!fname) return 'file entries require fname';
|
||||
|
||||
if (entry.content && typeof entry.content === 'string') {
|
||||
const b64 = sanitizeBase64(entry.content);
|
||||
if (!b64) return `file ${fname} is missing content`;
|
||||
const fkey = crypto.randomUUID();
|
||||
const target = path.join(FILES_DIR, fkey);
|
||||
|
||||
const fkey = crypto.randomUUID(),
|
||||
target = path.join(FILES_DIR, fkey);
|
||||
try {
|
||||
fs.writeFileSync(target, Buffer.from(b64, 'base64'));
|
||||
} catch (err) {
|
||||
console.error('failed to store file', fname, err);
|
||||
return `failed to store file ${fname}`;
|
||||
}
|
||||
|
||||
saved.push({ fname, fkey });
|
||||
continue;
|
||||
}
|
||||
@@ -382,7 +153,7 @@ function scheduleOrReplace(defInput: ollamaInp) {
|
||||
const key = def.name
|
||||
if (tasks.has(key)) { try { tasks.get(key).task.stop() } catch { } tasks.delete(key) }
|
||||
|
||||
const startTs = def.startAt ? new Date(def.startAt).getTime() : null;
|
||||
const startTs = def.startAt ? new Date(def.startAt).getTime() : null;
|
||||
|
||||
// forbid overlapping runs per schedule (like argo's Forbid)
|
||||
let running = false
|
||||
@@ -505,7 +276,7 @@ const server = http.createServer(async (req, res) => {
|
||||
allowed = ['/', ...fs.readdirSync(publicDir).map(f => `/${f}`)];
|
||||
|
||||
if (req.method === 'GET' && pathname === '/openapi.json') {
|
||||
return res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(openApiSpec, null, 2));
|
||||
return res.writeHead(200, { 'content-type': 'application/json' }).end(openApiSpec);
|
||||
}
|
||||
|
||||
// list schedules for the calling user
|
||||
@@ -591,6 +362,7 @@ const server = http.createServer(async (req, res) => {
|
||||
|
||||
const userId = await fetchUserID(req),
|
||||
nameParam = decodeURIComponent(paramRaw),
|
||||
|
||||
// stored names are already scoped; accept either raw or scoped
|
||||
key = tasks.has(nameParam) ? nameParam : scopedName(nameParam, userId),
|
||||
existing = tasks.get(key)?.def;
|
||||
|
||||
Reference in New Issue
Block a user