refactor: improve database initialization and news fetching structure
This commit is contained in:
File diff suppressed because it is too large
Load Diff
34
backend/app/schema.sql
Normal file
34
backend/app/schema.sql
Normal 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
|
||||
);
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
@@ -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})
|
||||
});
|
||||
|
27
frontend/src/components/NewsRefreshButton.vue
Normal file
27
frontend/src/components/NewsRefreshButton.vue
Normal 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>
|
@@ -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; // 30‑min 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 [];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -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: {
|
||||
|
Reference in New Issue
Block a user