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
|
# URL for the Ollama service
|
||||||
OLLAMA_HOST=http://localhost:11434
|
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
|
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
|
# 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 FeedManager from './components/FeedManager.vue';
|
||||||
import CronSlider from './components/CronSlider.vue';
|
import CronSlider from './components/CronSlider.vue';
|
||||||
import SyncButton from './components/SyncButton.vue';
|
import SyncButton from './components/SyncButton.vue';
|
||||||
|
import NewsRefreshButton from './components/NewsRefreshButton.vue';
|
||||||
import ModelStatus from './components/ModelStatus.vue';
|
import ModelStatus from './components/ModelStatus.vue';
|
||||||
|
|
||||||
const news = useNews();
|
const news = useNews();
|
||||||
@@ -12,31 +13,28 @@ const filters = ref({country: 'DE'});
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await news.loadLastSync();
|
await news.loadLastSync();
|
||||||
await news.sync(filters.value);
|
await news.sync(filters.value);
|
||||||
|
await news.getNews(filters.value);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<main class="max-w-4xl mx-auto p-4 space-y-6">
|
<main class="max-w-4xl mx-auto p-4 space-y-6">
|
||||||
<h1 class="text-2xl font-bold">📰 Local News Summariser</h1>
|
<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/>
|
<CronSlider/>
|
||||||
<SyncButton/>
|
<SyncButton/>
|
||||||
|
<NewsRefreshButton/>
|
||||||
<ModelStatus/>
|
<ModelStatus/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FeedManager/>
|
<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">
|
<article v-for="a in news.articles" :key="a.id" class="bg-white rounded p-4 shadow">
|
||||||
<h2 class="font-semibold">{{ a.title }}</h2>
|
<h2 class="font-semibold">{{ a.title }}</h2>
|
||||||
<p class="text-sm text-gray-600">{{ new Date(a.published).toLocaleString() }} – {{
|
<p class="text-sm text-gray-600">{{ new Date(a.published).toLocaleString() }} – {{ a.country }}</p>
|
||||||
a.source
|
<p>{{a.published}}</p>
|
||||||
}}</p>
|
<p class="mt-2">{{ a.description }}</p>
|
||||||
<p class="mt-2">{{ a.summary_de }}</p>
|
<p class="italic mt-2 text-sm text-gray-700">Added: {{ new Date(a.created_at).toLocaleString() }}</p>
|
||||||
<p class="italic mt-2 text-sm text-gray-700">{{ a.summary_en }}</p>
|
|
||||||
<a :href="a.url" target="_blank" class="text-blue-600 hover:underline">Original →</a>
|
<a :href="a.url" target="_blank" class="text-blue-600 hover:underline">Original →</a>
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
|
@@ -12,7 +12,7 @@ onMounted(async () => {
|
|||||||
async function update() {
|
async function update() {
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
await fetch('/settings/cron', {
|
await fetch('/settings/cron', {
|
||||||
method: 'PUT',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({hours: hours.value})
|
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 {defineStore} from 'pinia';
|
||||||
import {set, get} from 'idb-keyval';
|
|
||||||
|
|
||||||
export const useNews = defineStore('news', {
|
export const useNews = defineStore('news', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
articles: [] as {
|
articles: [] as {
|
||||||
id: number,
|
id: number,
|
||||||
published: number,
|
|
||||||
title: string,
|
title: string,
|
||||||
|
description: string,
|
||||||
url: string,
|
url: string,
|
||||||
source: string,
|
published: number,
|
||||||
summary_de: string,
|
country: string,
|
||||||
summary_en: string
|
created_at: number
|
||||||
}[], lastSync: 0, offline: false
|
}[], lastSync: 0
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async loadLastSync() {
|
async loadLastSync() {
|
||||||
@@ -22,23 +21,31 @@ export const useNews = defineStore('news', {
|
|||||||
return Date.now() - this.lastSync > 30 * 60 * 1000; // 30‑min guard
|
return Date.now() - this.lastSync > 30 * 60 * 1000; // 30‑min guard
|
||||||
},
|
},
|
||||||
async sync(filters: Record<string, string>) {
|
async sync(filters: Record<string, string>) {
|
||||||
try {
|
if (!this.canManualSync()) {
|
||||||
if (!this.canManualSync()) throw new Error('Too soon');
|
console.log('Too soon to sync again');
|
||||||
const q = new URLSearchParams(filters).toString();
|
return;
|
||||||
const res = await fetch(`/news?${q}`);
|
}
|
||||||
if (!res.ok) throw new Error('network');
|
|
||||||
|
const q = new URLSearchParams(filters).toString();
|
||||||
|
const res = await fetch(`/news?${q}`);
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
this.articles = data;
|
this.articles = data;
|
||||||
this.lastSync = Date.now();
|
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 vueJsx from '@vitejs/plugin-vue-jsx';
|
||||||
import {defineConfig} from 'vite';
|
import {defineConfig} from 'vite';
|
||||||
import vueDevTools from 'vite-plugin-vue-devtools';
|
import vueDevTools from 'vite-plugin-vue-devtools';
|
||||||
import {VitePWA} from "vite-plugin-pwa";
|
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@@ -13,22 +12,7 @@ export default defineConfig({
|
|||||||
vue(),
|
vue(),
|
||||||
vueJsx(),
|
vueJsx(),
|
||||||
vueDevTools(),
|
vueDevTools(),
|
||||||
tailwindcss(),
|
tailwindcss()
|
||||||
VitePWA({
|
|
||||||
registerType: 'autoUpdate',
|
|
||||||
workbox: {
|
|
||||||
runtimeCaching: [
|
|
||||||
{
|
|
||||||
urlPattern: /\/news\?/,
|
|
||||||
handler: 'NetworkFirst',
|
|
||||||
options: {
|
|
||||||
cacheName: 'news-api',
|
|
||||||
networkTimeoutSeconds: 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
],
|
],
|
||||||
build: {outDir: 'dist'},
|
build: {outDir: 'dist'},
|
||||||
resolve: {
|
resolve: {
|
||||||
|
Reference in New Issue
Block a user