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
+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;