refactor: improve database initialization and news fetching structure

This commit is contained in:
2025-08-01 21:57:13 +02:00
parent 3a1c817381
commit e22f3a627a
8 changed files with 552 additions and 400 deletions

File diff suppressed because it is too large Load Diff

34
backend/app/schema.sql Normal file
View File

@@ -0,0 +1,34 @@
-- Database schema for Owly News Summariser
-- News table to store articles
CREATE TABLE IF NOT EXISTS news (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
url TEXT NOT NULL,
published TIMESTAMP NOT NULL,
country TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Index for faster queries on published date
CREATE INDEX IF NOT EXISTS idx_news_published ON news(published);
-- Feeds table to store RSS feed sources
CREATE TABLE IF NOT EXISTS feeds (
id INTEGER PRIMARY KEY,
country TEXT,
url TEXT UNIQUE NOT NULL
);
-- Settings table for application configuration
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
val TEXT NOT NULL
);
-- Meta table for application metadata
CREATE TABLE IF NOT EXISTS meta (
key TEXT PRIMARY KEY,
val TEXT NOT NULL
);

View File

@@ -1,8 +1,29 @@
# URL for the Ollama service
OLLAMA_HOST=http://localhost:11434
# Interval for scheduled news fetching in hours (minimum: 0.5)
# Interval for scheduled news fetching in hours
CRON_HOURS=1
# Minimum interval for scheduled news fetching in hours
MIN_CRON_HOURS=0.5
# Cooldown period in minutes between manual syncs
SYNC_COOLDOWN_MINUTES=30
# LLM model to use for summarization
LLM_MODEL=qwen2:7b-instruct-q4_K_M
# Timeout in seconds for LLM requests
LLM_TIMEOUT_SECONDS=180
# Timeout in seconds for Ollama API requests
OLLAMA_API_TIMEOUT_SECONDS=10
# Timeout in seconds for article fetching
ARTICLE_FETCH_TIMEOUT=30
# Maximum length of article content to process
MAX_ARTICLE_LENGTH=5000
# SQLite database connection string
DATABASE_URL=sqlite:///./newsdb.sqlite
DB_NAME=owlynews.sqlite3

View File

@@ -4,6 +4,7 @@ import {useNews} from './stores/useNews';
import FeedManager from './components/FeedManager.vue';
import CronSlider from './components/CronSlider.vue';
import SyncButton from './components/SyncButton.vue';
import NewsRefreshButton from './components/NewsRefreshButton.vue';
import ModelStatus from './components/ModelStatus.vue';
const news = useNews();
@@ -12,31 +13,28 @@ const filters = ref({country: 'DE'});
onMounted(async () => {
await news.loadLastSync();
await news.sync(filters.value);
await news.getNews(filters.value);
});
</script>
<template>
<main class="max-w-4xl mx-auto p-4 space-y-6">
<h1 class="text-2xl font-bold">📰 Local News Summariser</h1>
<div class="grid md:grid-cols-3 gap-4">
<div class="grid md:grid-cols-4 gap-4">
<CronSlider/>
<SyncButton/>
<NewsRefreshButton/>
<ModelStatus/>
</div>
<FeedManager/>
<section v-if="news.offline" class="p-2 bg-yellow-100 border-l-4 border-yellow-500">
Offline Datenstand: {{ new Date(news.lastSync).toLocaleString() }}
</section>
<article v-for="a in news.articles" :key="a.id" class="bg-white rounded p-4 shadow">
<h2 class="font-semibold">{{ a.title }}</h2>
<p class="text-sm text-gray-600">{{ new Date(a.published).toLocaleString() }} {{
a.source
}}</p>
<p class="mt-2">{{ a.summary_de }}</p>
<p class="italic mt-2 text-sm text-gray-700">{{ a.summary_en }}</p>
<p class="text-sm text-gray-600">{{ new Date(a.published).toLocaleString() }} {{ a.country }}</p>
<p>{{a.published}}</p>
<p class="mt-2">{{ a.description }}</p>
<p class="italic mt-2 text-sm text-gray-700">Added: {{ new Date(a.created_at).toLocaleString() }}</p>
<a :href="a.url" target="_blank" class="text-blue-600 hover:underline">Original </a>
</article>
</main>

View File

@@ -12,7 +12,7 @@ onMounted(async () => {
async function update() {
saving.value = true;
await fetch('/settings/cron', {
method: 'PUT',
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({hours: hours.value})
});

View File

@@ -0,0 +1,27 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useNews } from '../stores/useNews';
const news = useNews();
const isLoading = ref(false);
async function refreshNews() {
isLoading.value = true;
try {
await news.getNews({country: 'DE'});
} finally {
isLoading.value = false;
}
}
</script>
<template>
<button
@click="refreshNews"
class="px-4 py-2 rounded bg-green-600 hover:bg-green-700 text-white flex items-center justify-center"
:disabled="isLoading"
>
<span v-if="isLoading" class="animate-spin mr-2"></span>
<span>Refresh News</span>
</button>
</template>

View File

@@ -1,17 +1,16 @@
import {defineStore} from 'pinia';
import {set, get} from 'idb-keyval';
export const useNews = defineStore('news', {
state: () => ({
articles: [] as {
id: number,
published: number,
title: string,
description: string,
url: string,
source: string,
summary_de: string,
summary_en: string
}[], lastSync: 0, offline: false
published: number,
country: string,
created_at: number
}[], lastSync: 0
}),
actions: {
async loadLastSync() {
@@ -22,23 +21,31 @@ export const useNews = defineStore('news', {
return Date.now() - this.lastSync > 30 * 60 * 1000; // 30min guard
},
async sync(filters: Record<string, string>) {
try {
if (!this.canManualSync()) throw new Error('Too soon');
if (!this.canManualSync()) {
console.log('Too soon to sync again');
return;
}
const q = new URLSearchParams(filters).toString();
const res = await fetch(`/news?${q}`);
if (!res.ok) throw new Error('network');
if (res.ok) {
const data = await res.json();
this.articles = data;
this.lastSync = Date.now();
await set(JSON.stringify(filters), data);
this.offline = false;
} catch (e) {
const cached = await get(JSON.stringify(filters));
if (cached) {
this.articles = cached;
this.offline = true;
}
},
async getNews(filters: Record<string, string> = {}) {
const q = new URLSearchParams(filters).toString();
const res = await fetch(`/news?${q}`);
if (res.ok) {
const data = await res.json();
this.articles = data;
return data;
}
return [];
}
}
});

View File

@@ -5,7 +5,6 @@ import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import {defineConfig} from 'vite';
import vueDevTools from 'vite-plugin-vue-devtools';
import {VitePWA} from "vite-plugin-pwa";
// https://vite.dev/config/
export default defineConfig({
@@ -13,22 +12,7 @@ export default defineConfig({
vue(),
vueJsx(),
vueDevTools(),
tailwindcss(),
VitePWA({
registerType: 'autoUpdate',
workbox: {
runtimeCaching: [
{
urlPattern: /\/news\?/,
handler: 'NetworkFirst',
options: {
cacheName: 'news-api',
networkTimeoutSeconds: 3
}
}
]
}
})
tailwindcss()
],
build: {outDir: 'dist'},
resolve: {