Compare commits

...

2 Commits

Author SHA1 Message Date
Sami Abuzakuk
c8aa5e9917 Add frontend support for settings 2025-10-12 10:22:20 +02:00
Sami Abuzakuk
288a40952e Add backend support for settings 2025-10-12 10:22:07 +02:00
7 changed files with 255 additions and 7 deletions

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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}`);

View File

@@ -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>

View File

@@ -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>

View 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>