Compare commits
2 Commits
6afc50eb81
...
c8aa5e9917
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8aa5e9917 | ||
|
|
288a40952e |
@@ -3,8 +3,8 @@ from fastapi import FastAPI
|
|||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from model import Log, SessionLocal, Script
|
from model import Log, SessionLocal, Script, Settings
|
||||||
from run_scripts import run_scripts
|
from run_scripts import run_scripts, update_requirements, update_environment
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
@@ -52,6 +52,76 @@ def hello():
|
|||||||
return {"message": "Welcome to the Project Monitor API"}
|
return {"message": "Welcome to the Project Monitor API"}
|
||||||
|
|
||||||
|
|
||||||
|
# Define Pydantic models for Settings
|
||||||
|
class SettingsBase(BaseModel):
|
||||||
|
requirements: str
|
||||||
|
environment: str
|
||||||
|
user: str
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsUpdate(SettingsBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsResponse(SettingsBase):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|
||||||
|
# Settings API Endpoints
|
||||||
|
@app.get("/settings", response_model=list[SettingsResponse])
|
||||||
|
def read_settings():
|
||||||
|
db = SessionLocal()
|
||||||
|
settings = db.query(Settings).all()
|
||||||
|
db.close()
|
||||||
|
return settings
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/settings", response_model=SettingsResponse)
|
||||||
|
def create_setting(settings: SettingsBase):
|
||||||
|
db = SessionLocal()
|
||||||
|
new_setting = Settings(**settings.model_dump())
|
||||||
|
db.add(new_setting)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(new_setting)
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
return new_setting
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/settings/{settings_id}", response_model=SettingsResponse)
|
||||||
|
def read_setting(settings_id: int):
|
||||||
|
db = SessionLocal()
|
||||||
|
setting = db.query(Settings).filter(Settings.id == settings_id).first()
|
||||||
|
db.close()
|
||||||
|
if not setting:
|
||||||
|
raise HTTPException(status_code=404, detail="Setting not found")
|
||||||
|
return setting
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/settings/{settings_id}", response_model=SettingsResponse)
|
||||||
|
def update_setting(settings_id: int, settings: SettingsUpdate):
|
||||||
|
db = SessionLocal()
|
||||||
|
existing_setting = db.query(Settings).filter(Settings.id == settings_id).first()
|
||||||
|
if not existing_setting:
|
||||||
|
raise HTTPException(status_code=404, detail="Setting not found")
|
||||||
|
|
||||||
|
if existing_setting.requirements != settings.requirements:
|
||||||
|
existing_setting.requirements = settings.requirements
|
||||||
|
update_requirements(settings)
|
||||||
|
|
||||||
|
if existing_setting.environment != settings.environment:
|
||||||
|
existing_setting.environment = settings.environment
|
||||||
|
update_environment(settings)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(existing_setting)
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
return existing_setting
|
||||||
|
|
||||||
|
|
||||||
@app.get("/script", response_model=list[ScriptResponse])
|
@app.get("/script", response_model=list[ScriptResponse])
|
||||||
def read_scripts():
|
def read_scripts():
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
|
|||||||
@@ -43,5 +43,14 @@ class Log(Base):
|
|||||||
script_id = Column(Integer, ForeignKey("scripts.id"), nullable=False)
|
script_id = Column(Integer, ForeignKey("scripts.id"), nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(Base):
|
||||||
|
__tablename__ = "user_settings"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
requirements = Column(String, nullable=False)
|
||||||
|
environment = Column(String, nullable=False)
|
||||||
|
user = Column(String, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
# Create the database tables
|
# Create the database tables
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|||||||
@@ -34,14 +34,42 @@ def run_scripts(script_ids: list[int] | None = None):
|
|||||||
|
|
||||||
def dump_script_to_file(script, filename):
|
def dump_script_to_file(script, filename):
|
||||||
with open(filename, "w") as file:
|
with open(filename, "w") as file:
|
||||||
|
file.write("from dotenv import load_dotenv\nload_dotenv()\n")
|
||||||
file.write(script.script_content)
|
file.write(script.script_content)
|
||||||
|
|
||||||
|
|
||||||
def execute_script(filename) -> subprocess.CompletedProcess:
|
def execute_script(filename) -> subprocess.CompletedProcess:
|
||||||
result = subprocess.run(["python", filename], capture_output=True, text=True)
|
result = subprocess.run(
|
||||||
|
["exec_folder/venv/bin/python", filename], capture_output=True, text=True
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def update_requirements(settings):
|
||||||
|
if settings is None:
|
||||||
|
raise ValueError("No default settings found")
|
||||||
|
|
||||||
|
# create requirements.txt
|
||||||
|
with open("exec_folder/requirements.txt", "w") as file:
|
||||||
|
file.write("dotenv\n")
|
||||||
|
file.write(settings.requirements)
|
||||||
|
|
||||||
|
# install requirements
|
||||||
|
subprocess.run(
|
||||||
|
["exec_folder/venv/bin/pip", "install", "-r", "exec_folder/requirements.txt"],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_environment(settings):
|
||||||
|
if settings is None:
|
||||||
|
raise ValueError("No default settings found")
|
||||||
|
|
||||||
|
# create .env file
|
||||||
|
with open("exec_folder/.env", "w") as file:
|
||||||
|
file.write(settings.environment)
|
||||||
|
|
||||||
|
|
||||||
def delete_script(filename):
|
def delete_script(filename):
|
||||||
try:
|
try:
|
||||||
os.remove(filename)
|
os.remove(filename)
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
export const API_URL = 'http://127.0.0.1:8000';
|
export const API_URL = 'http://127.0.0.1:8000';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type definitions for Settings
|
||||||
|
*/
|
||||||
|
export interface Settings {
|
||||||
|
id: number;
|
||||||
|
requirements: string;
|
||||||
|
environment: string;
|
||||||
|
user: string;
|
||||||
|
}
|
||||||
|
|
||||||
export async function checkHealth(): Promise<'healthy' | 'unhealthy'> {
|
export async function checkHealth(): Promise<'healthy' | 'unhealthy'> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_URL}/health`);
|
const response = await fetch(`${API_URL}/health`);
|
||||||
@@ -58,6 +68,42 @@ export async function addScript(
|
|||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch all settings
|
||||||
|
export async function fetchSettings(): Promise<Settings[]> {
|
||||||
|
const response = await fetch(`${API_URL}/settings`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch settings ' + response.statusText);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch a single setting by ID
|
||||||
|
export async function fetchSettingById(id: number): Promise<Settings> {
|
||||||
|
const response = await fetch(`${API_URL}/settings/${id}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch setting');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update an existing setting
|
||||||
|
export async function updateSetting(
|
||||||
|
id: number,
|
||||||
|
updatedSetting: Partial<Settings>
|
||||||
|
): Promise<Settings> {
|
||||||
|
const response = await fetch(`${API_URL}/settings/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(updatedSetting)
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to update setting');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch a single script by ID
|
// Fetch a single script by ID
|
||||||
export async function fetchScriptById(id: number): Promise<Script> {
|
export async function fetchScriptById(id: number): Promise<Script> {
|
||||||
const response = await fetch(`${API_URL}/script/${id}`);
|
const response = await fetch(`${API_URL}/script/${id}`);
|
||||||
|
|||||||
@@ -43,6 +43,9 @@
|
|||||||
<div class="flex space-x-6">
|
<div class="flex space-x-6">
|
||||||
<a href="/" class="text-lg hover:text-gray-400">Home</a>
|
<a href="/" class="text-lg hover:text-gray-400">Home</a>
|
||||||
<a href="/scripts" class="text-lg hover:text-gray-400">Scripts</a>
|
<a href="/scripts" class="text-lg hover:text-gray-400">Scripts</a>
|
||||||
|
<a href="/settings" class="text-lg hover:text-gray-400">
|
||||||
|
<Icon icon="material-symbols:settings" width="24" height="24" />
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { updateScript, deleteScript, addLog, deleteLog, executeScript } from '$lib/api';
|
import {
|
||||||
|
updateScript,
|
||||||
|
deleteScript,
|
||||||
|
addLog,
|
||||||
|
deleteLog,
|
||||||
|
executeScript,
|
||||||
|
fetchLogs
|
||||||
|
} from '$lib/api';
|
||||||
import type { Script, Log } from '$lib/api';
|
import type { Script, Log } from '$lib/api';
|
||||||
import CodeMirror from 'svelte-codemirror-editor';
|
import CodeMirror from 'svelte-codemirror-editor';
|
||||||
import { python } from '@codemirror/lang-python';
|
import { python } from '@codemirror/lang-python';
|
||||||
@@ -29,12 +36,14 @@
|
|||||||
selectedLog = null;
|
selectedLog = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notifications are now handled globally via the layout
|
|
||||||
|
|
||||||
async function handleExecuteScript() {
|
async function handleExecuteScript() {
|
||||||
try {
|
try {
|
||||||
await executeScript(script.id);
|
await executeScript(script.id);
|
||||||
window.showNotification('success', 'Script executed successfully!');
|
window.showNotification('success', 'Script executed successfully!');
|
||||||
|
// Reload the list of logs after execution
|
||||||
|
logs = (await fetchLogs(script.id)).sort(
|
||||||
|
(a, b) => new Date(b.created_at!).getTime() - new Date(a.created_at!).getTime()
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.showNotification('error', 'Failed to execute script. ' + err);
|
window.showNotification('error', 'Failed to execute script. ' + err);
|
||||||
}
|
}
|
||||||
@@ -252,7 +261,7 @@
|
|||||||
<div
|
<div
|
||||||
class="fixed inset-0 bg-opacity-30 backdrop-blur-sm flex items-center justify-center z-50"
|
class="fixed inset-0 bg-opacity-30 backdrop-blur-sm flex items-center justify-center z-50"
|
||||||
>
|
>
|
||||||
<div class="bg-white p-6 rounded shadow-lg w-3/4 max-w-2xl">
|
<div class="bg-white p-6 rounded shadow-lg w-3/4 max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||||
<h3 class="text-lg font-bold mb-4">Log Details</h3>
|
<h3 class="text-lg font-bold mb-4">Log Details</h3>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<p class="font-semibold">Message:</p>
|
<p class="font-semibold">Message:</p>
|
||||||
|
|||||||
83
frontend/src/routes/settings/+page.svelte
Normal file
83
frontend/src/routes/settings/+page.svelte
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { fetchSettings, updateSetting } from '$lib/api';
|
||||||
|
import type { Settings } from '$lib/api';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import CodeMirror from 'svelte-codemirror-editor';
|
||||||
|
|
||||||
|
let settings = writable<Settings[]>([]);
|
||||||
|
let isLoading = writable(false);
|
||||||
|
let error = writable<string | null>(null);
|
||||||
|
|
||||||
|
async function loadSettings() {
|
||||||
|
isLoading.set(true);
|
||||||
|
error.set(null);
|
||||||
|
try {
|
||||||
|
const data = await fetchSettings();
|
||||||
|
settings.set(data);
|
||||||
|
} catch (err) {
|
||||||
|
error.set('Failed to load settings');
|
||||||
|
} finally {
|
||||||
|
isLoading.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSetting(setting: Settings) {
|
||||||
|
isLoading.set(true);
|
||||||
|
error.set(null);
|
||||||
|
try {
|
||||||
|
await updateSetting(setting.id, setting);
|
||||||
|
loadSettings(); // Refresh settings after update
|
||||||
|
} catch (err) {
|
||||||
|
error.set('Failed to save setting');
|
||||||
|
} finally {
|
||||||
|
isLoading.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
loadSettings();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container mx-auto p-4">
|
||||||
|
<h1 class="text-2xl font-bold mb-4">Settings</h1>
|
||||||
|
|
||||||
|
{#if $isLoading}
|
||||||
|
<p>Loading...</p>
|
||||||
|
{:else if $error}
|
||||||
|
<p class="text-red-500">{$error}</p>
|
||||||
|
{:else}
|
||||||
|
<div class="space-y-4">
|
||||||
|
{#each $settings as setting (setting.id)}
|
||||||
|
<div class="p-4 border rounded shadow">
|
||||||
|
<label class="block mb-2 font-bold">Requirements</label>
|
||||||
|
<div class="w-full border rounded">
|
||||||
|
<CodeMirror bind:value={setting.requirements} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="block mt-4 mb-2 font-bold">Environment</label>
|
||||||
|
<div class="w-full border rounded">
|
||||||
|
<CodeMirror bind:value={setting.environment} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="block mt-4 mb-2 font-bold">User</label>
|
||||||
|
<input type="text" class="w-full p-2 border rounded" bind:value={setting.user} readonly />
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||||
|
on:click={() => saveSetting(setting)}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user