added nextcloud

This commit is contained in:
ION606
2025-10-03 08:41:37 -04:00
parent 133ef3f48b
commit e954bf82fb
10 changed files with 1398 additions and 377 deletions
+357
View File
@@ -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
View File
@@ -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;