Compare commits
32 Commits
f22259b863
...
rewrite-ru
Author | SHA1 | Date | |
---|---|---|---|
57a7b42b9d | |||
d37daf02f6 | |||
16167d18ff | |||
7c6724800f | |||
af304266a4 | |||
815e3b22fd | |||
e8e61faf61 | |||
c19813cbe2 | |||
cf163082b2 | |||
011b256662 | |||
0a97a57c76 | |||
338b3ac7c1 | |||
13fbac5009 | |||
092c065809 | |||
9b805e891a | |||
78073d27d7 | |||
c3b0c87bfa | |||
0aa8d9fa3a | |||
cbbd0948e6 | |||
3a5b0d8f4b | |||
0ce916c654 | |||
f853213d15 | |||
300845c655 | |||
d90c618ee3 | |||
e7a97206a9 | |||
c2adfa711d | |||
b2d82892ef | |||
0f1632ad65 | |||
7b114a6145 | |||
4edb2b2179 | |||
aa520efb82 | |||
e23a8d53d9 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -59,3 +59,8 @@ htmlcov/
|
|||||||
!.yarn/releases
|
!.yarn/releases
|
||||||
!.yarn/sdks
|
!.yarn/sdks
|
||||||
!.yarn/versions
|
!.yarn/versions
|
||||||
|
backend-rust/owlynews.sqlite3
|
||||||
|
backend-rust/target
|
||||||
|
/backend-rust/config.toml
|
||||||
|
/backend-rust/owlynews.sqlite3-shm
|
||||||
|
/backend-rust/owlynews.sqlite3-wal
|
||||||
|
236
README.md
236
README.md
@@ -1,105 +1,67 @@
|
|||||||
# Owly News Summariser
|
# Owly News
|
||||||
|
|
||||||
Owly News Summariser is a web application that fetches news articles from various RSS feeds, uses an LLM (Large Language Model) to summarize them, and presents them in a clean, user-friendly interface.
|
Owly News is a modern web application that fetches news articles from various RSS feeds, uses an LLM (Large Language Model) to summarize them, and presents them in a clean, user-friendly interface.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Fetches news from configurable RSS feeds
|
- Fetches news from configurable RSS feeds
|
||||||
- Automatically summarizes articles using Ollama LLM
|
- Automatically summarizes articles using Ollama LLM
|
||||||
- Filters news by country
|
- **AI-powered intelligent tagging** with geographic, category, and source tags
|
||||||
|
- **Advanced multi-criteria filtering** with hierarchical tag support
|
||||||
- Progressive Web App (PWA) support for offline access
|
- Progressive Web App (PWA) support for offline access
|
||||||
- Scheduled background updates
|
- Scheduled background updates
|
||||||
|
- High-performance Rust backend for optimal resource usage
|
||||||
|
- Modern Vue.js frontend with TypeScript support
|
||||||
|
- **Comprehensive analytics** and reading statistics
|
||||||
|
- **Flexible sharing system** with multiple format options
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
The project consists of two main components:
|
The project consists of multiple components:
|
||||||
|
|
||||||
- **Backend**: A FastAPI application that fetches and processes news feeds, summarizes articles, and provides API endpoints
|
- **Backend (Rust)**: Primary backend written in Rust using Axum framework for high performance (`backend-rust/`)
|
||||||
- **Frontend**: A Vue.js application that displays the news and provides a user interface for managing feeds
|
- **Backend (Python)**: Legacy FastAPI backend (`backend/`)
|
||||||
|
- **Frontend**: Modern Vue.js 3 application with TypeScript and Tailwind CSS (`frontend/`)
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Python 3.8+ for the backend
|
### For Rust Backend (Recommended)
|
||||||
- Node.js 16+ and Yarn for the frontend
|
- Rust 1.88.0+
|
||||||
|
- [Ollama](https://ollama.ai/) for article summarization and tagging
|
||||||
|
- SQLite (handled automatically by SQLx)
|
||||||
|
|
||||||
|
### For Python Backend (Legacy)
|
||||||
|
- Python 3.8+
|
||||||
- [Ollama](https://ollama.ai/) for article summarization
|
- [Ollama](https://ollama.ai/) for article summarization
|
||||||
|
|
||||||
## Installing Yarn
|
### For Frontend
|
||||||
|
- Node.js 22+ and npm
|
||||||
Yarn is a package manager for JavaScript that's required for the frontend. Here's how to install it:
|
- Modern web browser with PWA support
|
||||||
|
|
||||||
### Using npm (recommended)
|
|
||||||
|
|
||||||
If you already have Node.js installed, the easiest way to install Yarn is via npm:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install -g yarn
|
|
||||||
```
|
|
||||||
|
|
||||||
### Platform-specific installations
|
|
||||||
|
|
||||||
#### Windows
|
|
||||||
|
|
||||||
- **Using Chocolatey**: `choco install yarn`
|
|
||||||
- **Using Scoop**: `scoop install yarn`
|
|
||||||
- **Manual installation**: Download and run the [installer](https://classic.yarnpkg.com/latest.msi)
|
|
||||||
|
|
||||||
#### macOS
|
|
||||||
|
|
||||||
- **Using Homebrew**: `brew install yarn`
|
|
||||||
- **Using MacPorts**: `sudo port install yarn`
|
|
||||||
|
|
||||||
#### Linux
|
|
||||||
|
|
||||||
- **Debian/Ubuntu**:
|
|
||||||
```bash
|
|
||||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
|
|
||||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
|
||||||
sudo apt update && sudo apt install yarn
|
|
||||||
```
|
|
||||||
|
|
||||||
- **CentOS/Fedora/RHEL**:
|
|
||||||
```bash
|
|
||||||
curl --silent --location https://dl.yarnpkg.com/rpm/yarn.repo | sudo tee /etc/yum.repos.d/yarn.repo
|
|
||||||
sudo yum install yarn
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Arch Linux**: `pacman -S yarn`
|
|
||||||
|
|
||||||
After installation, verify Yarn is installed correctly:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn --version
|
|
||||||
```
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
### Backend Setup
|
### Rust Backend Setup (Recommended)
|
||||||
|
|
||||||
1. Navigate to the backend directory:
|
1. Navigate to the Rust backend directory:
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
cd backend-rust
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Create a virtual environment:
|
2. Create a `.env` file based on the example:
|
||||||
```bash
|
|
||||||
python -m venv venv
|
|
||||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Install dependencies:
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Create a `.env` file based on the example:
|
|
||||||
```bash
|
```bash
|
||||||
cp example.env .env
|
cp example.env .env
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Customize the `.env` file as needed:
|
3. Customize the `.env` file as needed:
|
||||||
- `OLLAMA_HOST`: URL for the Ollama service (default: http://localhost:11434)
|
|
||||||
- `CRON_HOURS`: Interval for scheduled news fetching (default: 1)
|
|
||||||
- `DATABASE_URL`: SQLite database connection string
|
- `DATABASE_URL`: SQLite database connection string
|
||||||
|
- `OLLAMA_BASE_URL`: URL for the Ollama service (default: http://localhost:11434)
|
||||||
|
- Other configuration options as documented in the example file
|
||||||
|
|
||||||
|
4. Run database migrations:
|
||||||
|
```bash
|
||||||
|
cargo install sqlx-cli
|
||||||
|
sqlx migrate run
|
||||||
|
```
|
||||||
|
|
||||||
### Frontend Setup
|
### Frontend Setup
|
||||||
|
|
||||||
@@ -110,29 +72,24 @@ yarn --version
|
|||||||
|
|
||||||
2. Install dependencies:
|
2. Install dependencies:
|
||||||
```bash
|
```bash
|
||||||
yarn
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running the Application
|
## Running the Application
|
||||||
|
|
||||||
### Running the Backend
|
### Running the Rust Backend
|
||||||
|
|
||||||
1. Navigate to the backend directory:
|
1. Navigate to the Rust backend directory:
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
cd backend-rust
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Activate the virtual environment:
|
2. Start the backend server:
|
||||||
```bash
|
```bash
|
||||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
cargo run
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Start the backend server:
|
The backend will be available at http://localhost:3000
|
||||||
```bash
|
|
||||||
uvicorn app.main:app --reload
|
|
||||||
```
|
|
||||||
|
|
||||||
The backend will be available at http://localhost:8000
|
|
||||||
|
|
||||||
### Running the Frontend
|
### Running the Frontend
|
||||||
|
|
||||||
@@ -143,22 +100,53 @@ yarn --version
|
|||||||
|
|
||||||
2. Start the development server:
|
2. Start the development server:
|
||||||
```bash
|
```bash
|
||||||
yarn dev:watch
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
The frontend will be available at http://localhost:5173
|
The frontend will be available at http://localhost:5173
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
### Intelligent Content Organization
|
||||||
|
- **AI-Powered Tagging**: Automatic classification with geographic, topical, and source tags
|
||||||
|
- **Hierarchical Filtering**: Multi-level filtering by location (country → region → city), categories, and content types
|
||||||
|
- **Smart Search**: Advanced filtering with suggestions based on tag relationships and usage patterns
|
||||||
|
- **Legacy Migration**: Seamless upgrade from simple country-based filtering to comprehensive tag-based system
|
||||||
|
|
||||||
|
### Advanced Analytics
|
||||||
|
- **Reading Statistics**: Track reading time, completion rates, and engagement patterns
|
||||||
|
- **Content Analytics**: Source performance, tag usage, and trending topics analysis
|
||||||
|
- **Geographic Insights**: Location-based content distribution and reading preferences
|
||||||
|
- **Goal Tracking**: Personal reading goals with progress monitoring
|
||||||
|
|
||||||
|
### Flexible Article Display
|
||||||
|
- **Compact View**: Title, excerpt, tags, and action buttons for quick browsing
|
||||||
|
- **On-Demand Loading**: Full content, AI summaries, and source links as needed
|
||||||
|
- **Visual Tag System**: Color-coded, hierarchical tags with click-to-filter functionality
|
||||||
|
- **Reading Status**: Visual indicators for read/unread status and progress tracking
|
||||||
|
|
||||||
|
### Enhanced Sharing
|
||||||
|
- **Multiple Formats**: Text, Markdown, HTML, and JSON export options
|
||||||
|
- **Custom Templates**: User-configurable sharing formats
|
||||||
|
- **One-Click Operations**: Copy to clipboard with formatted content
|
||||||
|
- **Privacy Controls**: Configurable information inclusion in shared content
|
||||||
|
|
||||||
## Building for Production
|
## Building for Production
|
||||||
|
|
||||||
### Building the Backend
|
### Building the Rust Backend
|
||||||
|
|
||||||
The backend can be deployed as a standard FastAPI application. You can use tools like Gunicorn with Uvicorn workers:
|
|
||||||
|
|
||||||
|
1. Navigate to the Rust backend directory:
|
||||||
```bash
|
```bash
|
||||||
pip install gunicorn
|
cd backend-rust
|
||||||
gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
2. Build the optimized release binary:
|
||||||
|
```bash
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
The binary will be available at `target/release/owly-news`
|
||||||
|
|
||||||
### Building the Frontend
|
### Building the Frontend
|
||||||
|
|
||||||
1. Navigate to the frontend directory:
|
1. Navigate to the frontend directory:
|
||||||
@@ -168,32 +156,62 @@ gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker
|
|||||||
|
|
||||||
2. Build the frontend:
|
2. Build the frontend:
|
||||||
```bash
|
```bash
|
||||||
yarn build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
The built files will be in the `dist` directory and can be served by any static file server.
|
The built files will be in the `dist` directory and can be served by any static file server.
|
||||||
|
|
||||||
## API Endpoints
|
## Development
|
||||||
|
|
||||||
The backend provides the following API endpoints:
|
### Code Quality
|
||||||
|
|
||||||
- `GET /news`: Get news articles with optional filtering
|
The project includes comprehensive tooling for code quality:
|
||||||
- `GET /meta/last_sync`: Get the timestamp of the last feed synchronization
|
|
||||||
- `POST /meta/cron`: Set the schedule for automatic feed synchronization
|
|
||||||
- `GET /meta/feeds`: List all configured feeds
|
|
||||||
- `POST /meta/feeds`: Add a new feed
|
|
||||||
- `DELETE /meta/feeds`: Delete a feed
|
|
||||||
- `GET /meta/model`: Check the status of the LLM model
|
|
||||||
- `POST /meta/sync`: Manually trigger a feed synchronization
|
|
||||||
|
|
||||||
## Environment Variables
|
**Frontend:**
|
||||||
|
- ESLint with Vue and TypeScript support
|
||||||
|
- Prettier for code formatting
|
||||||
|
- Vitest for testing
|
||||||
|
- TypeScript for type safety
|
||||||
|
- Oxlint for additional linting
|
||||||
|
|
||||||
### Backend
|
**Backend (Rust):**
|
||||||
|
- Standard Rust tooling (`cargo fmt`, `cargo clippy`)
|
||||||
|
- SQLx for compile-time checked SQL queries
|
||||||
|
|
||||||
- `OLLAMA_HOST`: URL for the Ollama service
|
### Testing
|
||||||
- `CRON_HOURS`: Interval for scheduled news fetching in hours
|
|
||||||
- `DATABASE_URL`: SQLite database connection string
|
|
||||||
|
|
||||||
## License
|
Run frontend tests:
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run test
|
||||||
|
```
|
||||||
|
|
||||||
Code ist unter der [PolyForm Noncommercial 1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0/) lizenziert. Für jegliche kommerzielle Nutzung bitte Kontakt aufnehmen.
|
## Configuration
|
||||||
|
|
||||||
|
The application uses a comprehensive configuration system via `config.toml`:
|
||||||
|
|
||||||
|
- **AI Settings**: Configure Ollama integration for summaries and tagging
|
||||||
|
- **Display Preferences**: Default views, themes, and UI customization
|
||||||
|
- **Analytics**: Control data collection and retention policies
|
||||||
|
- **Filtering**: Smart suggestions, saved filters, and geographic hierarchy
|
||||||
|
- **Sharing**: Default formats and custom templates
|
||||||
|
|
||||||
|
See the example configuration in the project for detailed options.
|
||||||
|
|
||||||
|
## Migration from Legacy Systems
|
||||||
|
|
||||||
|
The application includes automatic migration tools for upgrading from simpler filtering systems:
|
||||||
|
|
||||||
|
- **Country Filter Migration**: Automatic conversion to hierarchical geographic tags
|
||||||
|
- **Data Preservation**: Maintains historical data during migration
|
||||||
|
- **Backward Compatibility**: Gradual transition with user control
|
||||||
|
- **Validation Tools**: Ensure data integrity throughout the migration process
|
||||||
|
|
||||||
|
## Future Roadmap
|
||||||
|
|
||||||
|
The project is evolving through three phases:
|
||||||
|
1. **Phase 1**: High-performance Rust backend with advanced filtering and analytics
|
||||||
|
2. **Phase 2**: CLI application for power users and automation
|
||||||
|
3. **Phase 3**: Migration to Dioxus for a full Rust stack
|
||||||
|
|
||||||
|
See `ROADMAP.md` for detailed development plans and architectural decisions.
|
||||||
|
1
backend-rust/.gitignore
vendored
1
backend-rust/.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
target/
|
target/
|
||||||
|
/config.toml
|
||||||
|
386
backend-rust/Cargo.lock
generated
386
backend-rust/Cargo.lock
generated
@@ -49,9 +49,35 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.98"
|
version = "1.0.99"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "api"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
|
"axum",
|
||||||
|
"once_cell",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sqlx",
|
||||||
|
"toml",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-trait"
|
||||||
|
version = "0.1.89"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atoi"
|
name = "atoi"
|
||||||
@@ -212,6 +238,20 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"api",
|
||||||
|
"dotenv",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"server",
|
||||||
|
"tokio",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@@ -227,16 +267,6 @@ version = "0.9.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "core-foundation"
|
|
||||||
version = "0.9.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
|
||||||
dependencies = [
|
|
||||||
"core-foundation-sys",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
@@ -292,6 +322,16 @@ dependencies = [
|
|||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "db"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"api",
|
||||||
|
"sqlx",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
version = "0.7.10"
|
version = "0.7.10"
|
||||||
@@ -353,16 +393,6 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "errno"
|
|
||||||
version = "0.3.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "etcetera"
|
name = "etcetera"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -385,12 +415,6 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fastrand"
|
|
||||||
version = "2.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flume"
|
name = "flume"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -414,21 +438,6 @@ version = "0.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "foreign-types"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
|
||||||
dependencies = [
|
|
||||||
"foreign-types-shared",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "foreign-types-shared"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -528,19 +537,7 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
"wasi",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "getrandom"
|
|
||||||
version = "0.3.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"r-efi",
|
|
||||||
"wasi 0.14.2+wasi-0.2.4",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -889,12 +886,6 @@ dependencies = [
|
|||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "linux-raw-sys"
|
|
||||||
version = "0.9.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -970,27 +961,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
|
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
"wasi",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "native-tls"
|
|
||||||
version = "0.2.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"log",
|
|
||||||
"openssl",
|
|
||||||
"openssl-probe",
|
|
||||||
"openssl-sys",
|
|
||||||
"schannel",
|
|
||||||
"security-framework",
|
|
||||||
"security-framework-sys",
|
|
||||||
"tempfile",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.46.0"
|
version = "0.46.0"
|
||||||
@@ -1063,72 +1037,12 @@ version = "1.21.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "openssl"
|
|
||||||
version = "0.10.73"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"cfg-if",
|
|
||||||
"foreign-types",
|
|
||||||
"libc",
|
|
||||||
"once_cell",
|
|
||||||
"openssl-macros",
|
|
||||||
"openssl-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "openssl-macros"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "openssl-probe"
|
|
||||||
version = "0.1.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "openssl-sys"
|
|
||||||
version = "0.9.109"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
"libc",
|
|
||||||
"pkg-config",
|
|
||||||
"vcpkg",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "overload"
|
name = "overload"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "owly-news-summariser"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"axum",
|
|
||||||
"dotenv",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"sqlx",
|
|
||||||
"tokio",
|
|
||||||
"toml",
|
|
||||||
"tracing",
|
|
||||||
"tracing-subscriber",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
@@ -1248,12 +1162,6 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "r-efi"
|
|
||||||
version = "5.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
@@ -1281,7 +1189,7 @@ version = "0.6.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.16",
|
"getrandom",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1337,6 +1245,20 @@ version = "0.8.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ring"
|
||||||
|
version = "0.17.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"getrandom",
|
||||||
|
"libc",
|
||||||
|
"untrusted",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rsa"
|
name = "rsa"
|
||||||
version = "0.9.8"
|
version = "0.9.8"
|
||||||
@@ -1364,16 +1286,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustls"
|
||||||
version = "1.0.8"
|
version = "0.23.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
|
checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"once_cell",
|
||||||
"errno",
|
"ring",
|
||||||
"libc",
|
"rustls-pki-types",
|
||||||
"linux-raw-sys",
|
"rustls-webpki",
|
||||||
"windows-sys 0.59.0",
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-pki-types"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
|
||||||
|
dependencies = [
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-webpki"
|
||||||
|
version = "0.103.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"untrusted",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1388,44 +1331,12 @@ version = "1.0.20"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "schannel"
|
|
||||||
version = "0.1.27"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
|
|
||||||
dependencies = [
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "security-framework"
|
|
||||||
version = "2.11.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"core-foundation",
|
|
||||||
"core-foundation-sys",
|
|
||||||
"libc",
|
|
||||||
"security-framework-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "security-framework-sys"
|
|
||||||
version = "2.14.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
|
|
||||||
dependencies = [
|
|
||||||
"core-foundation-sys",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.219"
|
version = "1.0.219"
|
||||||
@@ -1489,6 +1400,25 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "server"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"api",
|
||||||
|
"axum",
|
||||||
|
"db",
|
||||||
|
"dotenv",
|
||||||
|
"http",
|
||||||
|
"once_cell",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sqlx",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
@@ -1624,9 +1554,9 @@ dependencies = [
|
|||||||
"indexmap",
|
"indexmap",
|
||||||
"log",
|
"log",
|
||||||
"memchr",
|
"memchr",
|
||||||
"native-tls",
|
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
|
"rustls",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
@@ -1636,6 +1566,8 @@ dependencies = [
|
|||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
|
"uuid",
|
||||||
|
"webpki-roots 0.26.11",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1716,6 +1648,7 @@ dependencies = [
|
|||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"uuid",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1754,6 +1687,7 @@ dependencies = [
|
|||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"uuid",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1780,6 +1714,7 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1833,19 +1768,6 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tempfile"
|
|
||||||
version = "3.20.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
|
|
||||||
dependencies = [
|
|
||||||
"fastrand",
|
|
||||||
"getrandom 0.3.3",
|
|
||||||
"once_cell",
|
|
||||||
"rustix",
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.12"
|
version = "2.0.12"
|
||||||
@@ -1911,7 +1833,6 @@ dependencies = [
|
|||||||
"io-uring",
|
"io-uring",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"parking_lot",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"slab",
|
"slab",
|
||||||
@@ -2104,6 +2025,12 @@ version = "0.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
|
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.4"
|
version = "2.5.4"
|
||||||
@@ -2121,6 +2048,16 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "1.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -2145,15 +2082,6 @@ version = "0.11.1+wasi-snapshot-preview1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasi"
|
|
||||||
version = "0.14.2+wasi-0.2.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
|
|
||||||
dependencies = [
|
|
||||||
"wit-bindgen-rt",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasite"
|
name = "wasite"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -2218,6 +2146,24 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "0.26.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||||
|
dependencies = [
|
||||||
|
"webpki-roots 1.0.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "whoami"
|
name = "whoami"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
@@ -2318,6 +2264,15 @@ dependencies = [
|
|||||||
"windows-targets 0.48.5",
|
"windows-targets 0.48.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.59.0"
|
version = "0.59.0"
|
||||||
@@ -2454,15 +2409,6 @@ version = "0.7.12"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
|
checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wit-bindgen-rt"
|
|
||||||
version = "0.39.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "writeable"
|
name = "writeable"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
|
@@ -1,16 +1,37 @@
|
|||||||
[package]
|
[workspace]
|
||||||
name = "owly-news-summariser"
|
members = [
|
||||||
version = "0.1.0"
|
"crates/api",
|
||||||
edition = "2024"
|
"crates/server",
|
||||||
|
"crates/cli",
|
||||||
|
"crates/db",
|
||||||
|
]
|
||||||
|
resolver = "3"
|
||||||
|
|
||||||
[dependencies]
|
[workspace.package]
|
||||||
anyhow = "1.0"
|
edition = "2024"
|
||||||
tokio = { version = "1", features = ["full"] }
|
version = "0.1.0"
|
||||||
axum = "0.8.4"
|
rust-version = "1.89"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = "1.0"
|
[workspace.dependencies]
|
||||||
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-native-tls", "sqlite", "macros", "migrate", "chrono", "json"] }
|
anyhow = "1.0.99"
|
||||||
dotenv = "0.15"
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
tracing = "0.1"
|
serde_json = "1.0.142"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tokio = { version = "1.47.1", features = ["rt-multi-thread", "macros", "signal"] }
|
||||||
|
libloading = "0.8.8"
|
||||||
|
tracing = "0.1.41"
|
||||||
|
once_cell = "1.21.3"
|
||||||
toml = "0.9.5"
|
toml = "0.9.5"
|
||||||
|
axum = "0.8.4"
|
||||||
|
sha2 = "0.10.9"
|
||||||
|
sqlx = { version = "0.8.6", default-features = false, features = ["runtime-tokio-rustls", "macros", "postgres", "uuid", "chrono", "migrate"] }
|
||||||
|
hex = "0.4.3"
|
||||||
|
num_cpus = "1.17.0"
|
||||||
|
unicode-segmentation = "1.12.0"
|
||||||
|
readability = "0.3.0"
|
||||||
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "fmt"] }
|
||||||
|
scraper = "0.23.1"
|
||||||
|
dotenv = "0.15.0"
|
||||||
|
|
||||||
|
# dev/test utilities in the workspace
|
||||||
|
tokio-test = "0.4.4"
|
||||||
|
axum-test = "17.3.0"
|
||||||
|
@@ -20,14 +20,22 @@ owly-news-summariser/
|
|||||||
│ ├── models/ # Data models & database entities
|
│ ├── models/ # Data models & database entities
|
||||||
│ │ ├── user.rs
|
│ │ ├── user.rs
|
||||||
│ │ ├── article.rs
|
│ │ ├── article.rs
|
||||||
│ │ └── summary.rs
|
│ │ ├── summary.rs
|
||||||
|
│ │ ├── tag.rs # Tag models and relationships
|
||||||
|
│ │ ├── analytics.rs # Analytics and statistics models
|
||||||
|
│ │ └── settings.rs # User settings and preferences
|
||||||
│ ├── services.rs # Services module declaration
|
│ ├── services.rs # Services module declaration
|
||||||
│ ├── services/ # Business logic layer
|
│ ├── services/ # Business logic layer
|
||||||
│ │ ├── news_service.rs
|
│ │ ├── news_service.rs
|
||||||
│ │ └── summary_service.rs
|
│ │ ├── summary_service.rs
|
||||||
|
│ │ ├── scraping_service.rs # Article content extraction
|
||||||
|
│ │ ├── tagging_service.rs # AI-powered tagging
|
||||||
|
│ │ ├── analytics_service.rs # Reading stats and analytics
|
||||||
|
│ │ └── sharing_service.rs # Article sharing functionality
|
||||||
│ └── config.rs # Configuration management
|
│ └── config.rs # Configuration management
|
||||||
├── migrations/ # SQLx migrations (managed by SQLx CLI)
|
├── migrations/ # SQLx migrations (managed by SQLx CLI)
|
||||||
├── frontend/ # Keep existing Vue frontend for now
|
├── frontend/ # Keep existing Vue frontend for now
|
||||||
|
├── config.toml # Configuration file with AI settings
|
||||||
└── Cargo.toml
|
└── Cargo.toml
|
||||||
```
|
```
|
||||||
### Phase 2: Multi-Binary Structure (API + CLI)
|
### Phase 2: Multi-Binary Structure (API + CLI)
|
||||||
@@ -42,6 +50,11 @@ owly-news-summariser/
|
|||||||
│ ├── [same module structure as Phase 1]
|
│ ├── [same module structure as Phase 1]
|
||||||
├── migrations/
|
├── migrations/
|
||||||
├── frontend/
|
├── frontend/
|
||||||
|
├── completions/ # Shell completion scripts
|
||||||
|
│ ├── owly.bash
|
||||||
|
│ ├── owly.zsh
|
||||||
|
│ └── owly.fish
|
||||||
|
├── config.toml
|
||||||
└── Cargo.toml # Updated for multiple binaries
|
└── Cargo.toml # Updated for multiple binaries
|
||||||
```
|
```
|
||||||
### Phase 3: Full Rust Stack
|
### Phase 3: Full Rust Stack
|
||||||
@@ -53,40 +66,249 @@ owly-news-summariser/
|
|||||||
├── migrations/
|
├── migrations/
|
||||||
├── frontend-dioxus/ # New Dioxus frontend
|
├── frontend-dioxus/ # New Dioxus frontend
|
||||||
├── frontend/ # Legacy Vue (to be removed)
|
├── frontend/ # Legacy Vue (to be removed)
|
||||||
|
├── completions/
|
||||||
|
├── config.toml
|
||||||
└── Cargo.toml
|
└── Cargo.toml
|
||||||
```
|
```
|
||||||
|
## Core Features & Architecture
|
||||||
|
|
||||||
|
### Article Processing & Display Workflow
|
||||||
|
**Hybrid Approach: RSS Feeds + Manual Submissions with Smart Content Management**
|
||||||
|
|
||||||
|
1. **Article Collection**
|
||||||
|
- RSS feed monitoring and batch processing
|
||||||
|
- Manual article URL submission
|
||||||
|
- Store original content and metadata in database
|
||||||
|
|
||||||
|
2. **Content Processing Pipeline**
|
||||||
|
- Fetch RSS articles → scrape full content → store in DB
|
||||||
|
- **Compact Article Display**:
|
||||||
|
- Title (primary display)
|
||||||
|
- RSS description text
|
||||||
|
- Tags (visual indicators)
|
||||||
|
- Time posted (from RSS)
|
||||||
|
- Time added (when added to system)
|
||||||
|
- Action buttons: [Full Article] [Summary] [Source]
|
||||||
|
- **On-Demand Content Loading**:
|
||||||
|
- Full Article: Display complete scraped content
|
||||||
|
- Summary: Show AI-generated summary
|
||||||
|
- Source: Open original URL in new tab
|
||||||
|
- Background async processing with status updates
|
||||||
|
- Support for re-processing without re-fetching
|
||||||
|
|
||||||
|
3. **Intelligent Tagging System**
|
||||||
|
- **Automatic Tag Generation**: AI analyzes content and assigns relevant tags
|
||||||
|
- **Geographic & Source Tags**: AI-generated location tags (countries, regions, cities) and publication source tags
|
||||||
|
- **Content Category Tags**: Technology, Politics, Business, Sports, Health, etc.
|
||||||
|
- **Visual Tag Display**: Color-coded tags in compact article view with hierarchical display
|
||||||
|
- **Tag Filtering**: Quick filtering by clicking tags with smart suggestions
|
||||||
|
- **Custom Tags**: User-defined tags and categories
|
||||||
|
- **Tag Confidence**: Visual indicators for AI vs manual tags
|
||||||
|
- **Tag Migration**: Automatic conversion of legacy country filters to geographic tags
|
||||||
|
|
||||||
|
4. **Analytics & Statistics System**
|
||||||
|
- **Reading Analytics**:
|
||||||
|
- Articles read vs added
|
||||||
|
- Reading time tracking
|
||||||
|
- Most read categories and tags
|
||||||
|
- Reading patterns over time
|
||||||
|
- **Content Analytics**:
|
||||||
|
- Source reliability and quality metrics
|
||||||
|
- Tag usage statistics
|
||||||
|
- Processing success rates
|
||||||
|
- Content freshness tracking
|
||||||
|
- **Performance Metrics**:
|
||||||
|
- AI processing times
|
||||||
|
- Scraping success rates
|
||||||
|
- User engagement patterns
|
||||||
|
|
||||||
|
5. **Advanced Filtering System**
|
||||||
|
- **Multi-Criteria Filtering**:
|
||||||
|
- By tags (single or multiple with AND/OR logic)
|
||||||
|
- By geographic tags (country, region, city with hierarchical filtering)
|
||||||
|
- By content categories and topics
|
||||||
|
- By date ranges (posted, added, read)
|
||||||
|
- By processing status (pending, completed, failed)
|
||||||
|
- By content availability (scraped, summary, RSS-only)
|
||||||
|
- By read/unread status
|
||||||
|
- **Smart Filter Migration**: Automatic conversion of legacy country filters to tag-based equivalents
|
||||||
|
- **Saved Filter Presets**:
|
||||||
|
- Custom filter combinations
|
||||||
|
- Quick access to frequent searches
|
||||||
|
- Geographic preset templates (e.g., "European Tech News", "US Politics")
|
||||||
|
- **Smart Suggestions**: Filter suggestions based on usage patterns and tag relationships
|
||||||
|
|
||||||
|
6. **Settings & Management System**
|
||||||
|
- **User Preferences**:
|
||||||
|
- Default article view mode
|
||||||
|
- Tag display preferences with geographic hierarchy settings
|
||||||
|
- Reading tracking settings
|
||||||
|
- Notification preferences
|
||||||
|
- **System Settings**:
|
||||||
|
- AI configuration (via API and config file)
|
||||||
|
- Processing settings
|
||||||
|
- Display customization
|
||||||
|
- Export preferences
|
||||||
|
- **Content Management**:
|
||||||
|
- Bulk operations (mark read, delete, retag)
|
||||||
|
- Archive old articles
|
||||||
|
- Export/import functionality
|
||||||
|
- Legacy data migration tools
|
||||||
|
|
||||||
|
7. **Article Sharing System**
|
||||||
|
- **Multiple Share Formats**:
|
||||||
|
- Clean text format with title, summary, and source link
|
||||||
|
- Markdown format for developers
|
||||||
|
- Rich HTML format for email/web
|
||||||
|
- JSON format for API integration
|
||||||
|
- **Copy to Clipboard**: One-click formatted sharing
|
||||||
|
- **Share Templates**: Customizable sharing formats
|
||||||
|
- **Privacy Controls**: Control what information is included in shares
|
||||||
|
|
||||||
|
8. **Database Schema**
|
||||||
|
```
|
||||||
|
Articles: id, title, url, source_type, rss_content, full_content,
|
||||||
|
summary, processing_status, published_at, added_at, read_at,
|
||||||
|
read_count, reading_time, ai_enabled, created_at, updated_at
|
||||||
|
Tags: id, name, category, description, color, usage_count, parent_id, created_at
|
||||||
|
ArticleTags: article_id, tag_id, confidence_score, ai_generated, created_at
|
||||||
|
ReadingStats: user_id, article_id, read_at, reading_time, completion_rate
|
||||||
|
FilterPresets: id, name, filter_criteria, user_id, created_at
|
||||||
|
Settings: key, value, category, user_id, updated_at
|
||||||
|
ShareTemplates: id, name, format, template_content, created_at
|
||||||
|
LegacyMigration: old_filter_type, old_value, new_tag_ids, migrated_at
|
||||||
|
```
|
||||||
|
|
||||||
## Step-by-Step Process
|
## Step-by-Step Process
|
||||||
|
|
||||||
### Phase 1: Axum API Implementation
|
### Phase 1: Axum API Implementation
|
||||||
|
|
||||||
**Step 1: Core Infrastructure Setup**
|
**Step 1: Core Infrastructure Setup**
|
||||||
- Set up database connection pooling with SQLx
|
- Set up database connection pooling with SQLx
|
||||||
- Create configuration management system (environment variables, config files)
|
- **Enhanced Configuration System**:
|
||||||
|
- Extend config.toml with comprehensive settings
|
||||||
|
- AI provider configurations with separate summary/tagging settings
|
||||||
|
- Display preferences and UI customization
|
||||||
|
- Analytics and tracking preferences
|
||||||
|
- Sharing templates and formats
|
||||||
|
- Filter and search settings
|
||||||
|
- Geographic tagging preferences
|
||||||
- Establish error handling patterns with `anyhow`
|
- Establish error handling patterns with `anyhow`
|
||||||
- Set up logging infrastructure
|
- Set up logging and analytics infrastructure
|
||||||
|
|
||||||
**Step 2: Data Layer**
|
**Step 2: Data Layer**
|
||||||
- Design your database schema and create SQLx migrations using `sqlx migrate add`
|
- Design comprehensive database schema with analytics and settings support
|
||||||
- Create Rust structs that mirror your Python backend's data models
|
- Create SQLx migrations for all tables including analytics and user preferences
|
||||||
- Implement database access layer with proper async patterns
|
- Implement hierarchical tag system with geographic and content categories
|
||||||
|
- Add legacy migration support for country filters
|
||||||
|
- Implement article models with reading tracking and statistics
|
||||||
|
- Add settings and preferences data layer
|
||||||
|
- Create analytics data models and aggregation queries
|
||||||
|
- Implement sharing templates and format management
|
||||||
- Use SQLx's compile-time checked queries
|
- Use SQLx's compile-time checked queries
|
||||||
|
|
||||||
**Step 3: API Layer Architecture**
|
**Step 3: Enhanced Services Layer**
|
||||||
- Create modular route structure (users, articles, summaries, etc.)
|
- **Content Processing Services**:
|
||||||
- Implement middleware for CORS, authentication, logging
|
- RSS feed fetching and parsing
|
||||||
- Set up request/response serialization with Serde
|
- Web scraping with quality tracking
|
||||||
- Create proper error responses and status codes
|
- AI services for summary and tagging
|
||||||
|
- **Enhanced Tagging Service**:
|
||||||
|
- Geographic location detection and tagging
|
||||||
|
- Content category classification
|
||||||
|
- Hierarchical tag relationships
|
||||||
|
- Legacy filter migration logic
|
||||||
|
- **Analytics Service**:
|
||||||
|
- Reading statistics collection and aggregation
|
||||||
|
- Content performance metrics
|
||||||
|
- User behavior tracking
|
||||||
|
- Trend analysis and insights
|
||||||
|
- **Settings Management Service**:
|
||||||
|
- User preference handling
|
||||||
|
- System configuration management
|
||||||
|
- Real-time settings updates
|
||||||
|
- **Sharing Service**:
|
||||||
|
- Multiple format generation
|
||||||
|
- Template processing
|
||||||
|
- Privacy-aware content filtering
|
||||||
|
- **Advanced Filtering Service**:
|
||||||
|
- Complex query building with geographic hierarchy
|
||||||
|
- Filter preset management
|
||||||
|
- Search optimization
|
||||||
|
- Legacy filter migration
|
||||||
|
|
||||||
**Step 4: Business Logic Migration**
|
**Step 4: Comprehensive API Layer**
|
||||||
- Port your Python backend logic to Rust services
|
- **Article Management Routes**:
|
||||||
- Maintain API compatibility with your existing Vue frontend
|
- `GET /api/articles` - List articles with compact display data
|
||||||
- Implement proper async patterns for external API calls
|
- `POST /api/articles` - Submit manual article URL
|
||||||
- Add comprehensive testing
|
- `GET /api/articles/:id` - Get basic article info
|
||||||
|
- `GET /api/articles/:id/full` - Get complete scraped content
|
||||||
|
- `GET /api/articles/:id/summary` - Get AI summary
|
||||||
|
- `POST /api/articles/:id/read` - Mark as read and track reading time
|
||||||
|
- `POST /api/articles/:id/share` - Generate shareable content
|
||||||
|
- **Analytics Routes**:
|
||||||
|
- `GET /api/analytics/dashboard` - Main analytics dashboard data
|
||||||
|
- `GET /api/analytics/reading-stats` - Personal reading statistics
|
||||||
|
- `GET /api/analytics/content-stats` - Content and source analytics
|
||||||
|
- `GET /api/analytics/trends` - Trending topics and patterns
|
||||||
|
- `GET /api/analytics/export` - Export analytics data
|
||||||
|
- **Enhanced Filtering & Search Routes**:
|
||||||
|
- `GET /api/filters/presets` - Get saved filter presets
|
||||||
|
- `POST /api/filters/presets` - Save new filter preset
|
||||||
|
- `GET /api/search/suggestions` - Get search and filter suggestions
|
||||||
|
- `POST /api/search` - Advanced search with multiple criteria
|
||||||
|
- `POST /api/filters/migrate` - Migrate legacy country filters to tags
|
||||||
|
- **Settings Routes**:
|
||||||
|
- `GET /api/settings` - Get all user settings
|
||||||
|
- `PUT /api/settings` - Update user settings
|
||||||
|
- `GET /api/settings/system` - Get system configuration
|
||||||
|
- `PUT /api/settings/system` - Update system settings (admin)
|
||||||
|
- **Enhanced Tag Management Routes**:
|
||||||
|
- `GET /api/tags` - List tags with usage statistics and hierarchy
|
||||||
|
- `GET /api/tags/geographic` - Get geographic tag hierarchy
|
||||||
|
- `GET /api/tags/trending` - Get trending tags
|
||||||
|
- `POST /api/tags/:id/follow` - Follow/unfollow tag for notifications
|
||||||
|
- `GET /api/tags/categories` - Get tag categories and relationships
|
||||||
|
- **Sharing Routes**:
|
||||||
|
- `GET /api/share/templates` - Get sharing templates
|
||||||
|
- `POST /api/share/templates` - Create custom sharing template
|
||||||
|
- `POST /api/articles/:id/share/:format` - Generate share content
|
||||||
|
|
||||||
**Step 5: Integration & Testing**
|
**Step 5: Enhanced Frontend Features**
|
||||||
- Test API endpoints thoroughly
|
- **Compact Article Display**:
|
||||||
- Ensure Vue frontend works seamlessly with new Rust backend
|
- Card-based layout with title, RSS excerpt, tags, and timestamps
|
||||||
- Performance testing and optimization
|
- Action buttons for Full Article, Summary, and Source
|
||||||
|
- Hierarchical tag display with geographic and category indicators
|
||||||
|
- Reading status and progress indicators
|
||||||
|
- **Advanced Analytics Dashboard**:
|
||||||
|
- Reading statistics with charts and trends
|
||||||
|
- Content source performance metrics
|
||||||
|
- Tag usage and trending topics with geographic breakdowns
|
||||||
|
- Personal reading insights and goals
|
||||||
|
- **Comprehensive Filtering Interface**:
|
||||||
|
- Multi-criteria filter builder with geographic hierarchy
|
||||||
|
- Saved filter presets with quick access
|
||||||
|
- Smart filter suggestions based on tag relationships
|
||||||
|
- Visual filter indicators and clear actions
|
||||||
|
- Legacy filter migration interface
|
||||||
|
- **Settings Management Panel**:
|
||||||
|
- User preference configuration
|
||||||
|
- AI and processing settings
|
||||||
|
- Display and UI customization
|
||||||
|
- Export/import functionality
|
||||||
|
- **Enhanced Sharing System**:
|
||||||
|
- Quick share buttons with format selection
|
||||||
|
- Copy-to-clipboard functionality
|
||||||
|
- Custom sharing templates
|
||||||
|
- Preview before sharing
|
||||||
|
|
||||||
|
**Step 6: Integration & Testing**
|
||||||
|
- Test all API endpoints with comprehensive coverage
|
||||||
|
- Test analytics collection and aggregation
|
||||||
|
- Test enhanced filtering and search functionality
|
||||||
|
- Test legacy filter migration
|
||||||
|
- Validate settings persistence and real-time updates
|
||||||
|
- Test sharing functionality across different formats
|
||||||
|
- Performance testing with large datasets and hierarchical tags
|
||||||
- Deploy and monitor
|
- Deploy and monitor
|
||||||
|
|
||||||
### Phase 2: CLI Application Addition
|
### Phase 2: CLI Application Addition
|
||||||
@@ -97,75 +319,266 @@ owly-news-summariser/
|
|||||||
- Keep shared logic in `src/lib.rs`
|
- Keep shared logic in `src/lib.rs`
|
||||||
- Update Cargo.toml to support multiple binaries
|
- Update Cargo.toml to support multiple binaries
|
||||||
|
|
||||||
**Step 2: CLI Architecture**
|
**Step 2: Enhanced CLI with Analytics and Management**
|
||||||
- Use clap for command-line argument parsing
|
- **Core Commands**:
|
||||||
- Reuse existing services and models from the API
|
- `owly list [--filters] [--format table|json|compact]` - List articles
|
||||||
- Create CLI-specific output formatting
|
- `owly show <id> [--content|--summary]` - Display specific article
|
||||||
- Implement batch processing capabilities
|
- `owly read <id>` - Mark article as read and open in pager
|
||||||
|
- `owly open <id>` - Open source URL in browser
|
||||||
|
- **Analytics Commands**:
|
||||||
|
- `owly stats [--period day|week|month|year]` - Show reading statistics
|
||||||
|
- `owly trends [--tags|--sources|--topics|--geo]` - Display trending content
|
||||||
|
- `owly analytics export [--format csv|json]` - Export analytics data
|
||||||
|
- **Management Commands**:
|
||||||
|
- `owly settings [--get key] [--set key=value]` - Manage settings
|
||||||
|
- `owly filters [--list|--save name|--load name]` - Manage filter presets
|
||||||
|
- `owly cleanup [--old|--unread|--failed]` - Clean up articles
|
||||||
|
- `owly migrate [--from-country-filters]` - Migrate legacy data
|
||||||
|
- **Enhanced Filtering Commands**:
|
||||||
|
- `owly filter [--tag] [--geo] [--category]` - Advanced filtering with geographic support
|
||||||
|
- `owly tags [--list|--hierarchy|--geo]` - Tag management with geographic display
|
||||||
|
- **Sharing Commands**:
|
||||||
|
- `owly share <id> [--format text|markdown|html]` - Generate share content
|
||||||
|
- `owly export <id> [--template name] [--output file]` - Export article
|
||||||
|
|
||||||
**Step 3: Shared Core Logic**
|
**Step 3: Advanced CLI Features**
|
||||||
- Extract common functionality into library crates
|
- Interactive filtering and search with geographic hierarchy
|
||||||
- Ensure both API and CLI can use the same business logic
|
- Real-time analytics display with charts (using ASCII graphs)
|
||||||
- Implement proper configuration management for both contexts
|
- Bulk operations with progress indicators
|
||||||
|
- Settings management with validation
|
||||||
|
- Shell completion for all commands and parameters
|
||||||
|
- Legacy data migration tools
|
||||||
|
|
||||||
### Phase 3: Dioxus Frontend Migration
|
### Phase 3: Dioxus Frontend Migration
|
||||||
|
|
||||||
**Step 1: Parallel Development**
|
**Step 1: Component Architecture**
|
||||||
- Create new `frontend-dioxus/` directory
|
- **Core Display Components**:
|
||||||
- Keep existing Vue frontend running during development
|
- `ArticleCard` - Compact article display with action buttons
|
||||||
- Set up Dioxus project structure with proper routing
|
- `ArticleViewer` - Full article content display
|
||||||
|
- `SummaryViewer` - AI summary display
|
||||||
|
- `TagCloud` - Interactive tag display with geographic hierarchy
|
||||||
|
- `GeographicMap` - Visual geographic filtering interface
|
||||||
|
- **Analytics Components**:
|
||||||
|
- `AnalyticsDashboard` - Main analytics overview
|
||||||
|
- `ReadingStats` - Personal reading statistics
|
||||||
|
- `TrendChart` - Trending topics and patterns
|
||||||
|
- `ContentMetrics` - Source and content analytics
|
||||||
|
- `GeographicAnalytics` - Location-based content insights
|
||||||
|
- **Enhanced Filtering Components**:
|
||||||
|
- `FilterBuilder` - Advanced filter creation interface with geographic support
|
||||||
|
- `FilterPresets` - Saved filter management
|
||||||
|
- `SearchBar` - Smart search with suggestions
|
||||||
|
- `GeographicFilter` - Hierarchical location filtering
|
||||||
|
- `MigrationTool` - Legacy filter migration interface
|
||||||
|
- **Settings Components**:
|
||||||
|
- `SettingsPanel` - User preference management
|
||||||
|
- `SystemConfig` - System-wide configuration
|
||||||
|
- `ExportImport` - Data export/import functionality
|
||||||
|
- **Sharing Components**:
|
||||||
|
- `ShareDialog` - Sharing interface with format options
|
||||||
|
- `ShareTemplates` - Custom template management
|
||||||
|
|
||||||
**Step 2: Component Architecture**
|
**Step 2: Enhanced UX Features**
|
||||||
- Design reusable Dioxus components
|
- **Smart Article Display**:
|
||||||
- Implement state management (similar to Pinia in Vue)
|
- Lazy loading for performance
|
||||||
- Create API client layer for communication with Rust backend
|
- Infinite scroll with virtualization
|
||||||
|
- Quick preview on hover
|
||||||
**Step 3: Feature Parity**
|
- Keyboard navigation support
|
||||||
- Port Vue components to Dioxus incrementally
|
- **Advanced Analytics**:
|
||||||
- Ensure UI/UX consistency
|
- Interactive charts and graphs with geographic data
|
||||||
- Implement proper error handling and loading states
|
- Customizable dashboard widgets
|
||||||
|
- Goal setting and progress tracking
|
||||||
**Step 4: Final Migration**
|
- Comparison and trend analysis
|
||||||
- Switch production traffic to Dioxus frontend
|
- **Intelligent Filtering**:
|
||||||
- Remove Vue frontend after thorough testing
|
- Auto-complete for filters with geographic suggestions
|
||||||
- Optimize bundle size and performance
|
- Visual filter builder with map integration
|
||||||
|
- Filter combination suggestions based on tag relationships
|
||||||
|
- Saved search notifications
|
||||||
|
- **Seamless Sharing**:
|
||||||
|
- One-click sharing with clipboard integration
|
||||||
|
- Live preview of shared content
|
||||||
|
- Social media format optimization
|
||||||
|
- Batch sharing capabilities
|
||||||
|
|
||||||
## Key Strategic Considerations
|
## Key Strategic Considerations
|
||||||
|
|
||||||
### 1. Modern Rust Practices
|
### 1. Performance & Scalability
|
||||||
- Use modern module structure without `mod.rs` files
|
- **Efficient Data Loading**: Lazy loading and pagination for large datasets
|
||||||
- Leverage SQLx's built-in migration and connection management
|
- **Optimized Queries**: Indexed database queries for filtering and analytics with hierarchical tag support
|
||||||
- Follow Rust 2018+ edition conventions
|
- **Caching Strategy**: Smart caching for frequently accessed content and tag hierarchies
|
||||||
|
- **Real-time Updates**: WebSocket integration for live analytics
|
||||||
|
|
||||||
### 2. Maintain Backward Compatibility
|
### 2. User Experience Focus
|
||||||
- Keep API contracts stable during Vue-to-Dioxus transition
|
- **Progressive Disclosure**: Show essential info first, details on demand
|
||||||
- Use feature flags for gradual rollouts
|
- **Responsive Design**: Optimized for mobile and desktop
|
||||||
|
- **Accessibility**: Full keyboard navigation and screen reader support
|
||||||
|
- **Customization**: User-configurable interface and behavior
|
||||||
|
- **Smooth Migration**: Seamless transition from country-based to tag-based filtering
|
||||||
|
|
||||||
### 3. Shared Code Architecture
|
### 3. Analytics & Insights
|
||||||
- Design your core business logic to be framework-agnostic
|
- **Privacy-First**: User control over data collection and retention
|
||||||
- Use workspace structure for better code organization
|
- **Actionable Insights**: Meaningful statistics that guide reading habits
|
||||||
- Consider extracting domain logic into separate crates
|
- **Performance Metrics**: System health and efficiency tracking
|
||||||
|
- **Trend Analysis**: Pattern recognition for content and behavior with geographic context
|
||||||
|
|
||||||
### 4. Testing Strategy
|
### 4. Content Management
|
||||||
- Unit tests for business logic
|
- **Flexible Display**: Multiple view modes for different use cases
|
||||||
- Integration tests for API endpoints
|
- **Smart Organization**: AI-assisted content categorization with geographic awareness
|
||||||
- End-to-end tests for the full stack
|
- **Bulk Operations**: Efficient management of large article collections
|
||||||
- CLI integration tests
|
- **Data Integrity**: Reliable content processing and error handling
|
||||||
|
- **Legacy Support**: Smooth migration from existing country-based filtering
|
||||||
|
|
||||||
### 5. Configuration Management
|
## Enhanced Configuration File Structure
|
||||||
- Environment-based configuration
|
|
||||||
- Support for different deployment scenarios (API-only, CLI-only, full stack)
|
|
||||||
- Proper secrets management
|
|
||||||
|
|
||||||
### 6. Database Strategy
|
```toml
|
||||||
- Use SQLx migrations for schema evolution (`sqlx migrate add/run`)
|
[server]
|
||||||
- Leverage compile-time checked queries with SQLx macros
|
host = '127.0.0.1'
|
||||||
- Implement proper connection pooling and error handling
|
port = 8090
|
||||||
- Let SQLx handle what it does best - don't reinvent the wheel
|
|
||||||
|
|
||||||
## What SQLx Handles for You
|
[display]
|
||||||
|
default_view = "compact" # compact, full, summary
|
||||||
|
articles_per_page = 50
|
||||||
|
show_reading_time = true
|
||||||
|
show_word_count = false
|
||||||
|
highlight_unread = true
|
||||||
|
theme = "auto" # light, dark, auto
|
||||||
|
|
||||||
- **Migrations**: Use `sqlx migrate add <name>` to create, `sqlx::migrate!()` macro to embed
|
[analytics]
|
||||||
- **Connection Pooling**: Built-in `SqlitePool` with configuration options
|
enabled = true
|
||||||
- **Query Safety**: Compile-time checked queries prevent SQL injection and typos
|
track_reading_time = true
|
||||||
- **Type Safety**: Automatic Rust type mapping from database types
|
track_scroll_position = true
|
||||||
|
retention_days = 365 # How long to keep detailed analytics
|
||||||
|
aggregate_older_data = true
|
||||||
|
|
||||||
|
[filtering]
|
||||||
|
enable_smart_suggestions = true
|
||||||
|
max_recent_filters = 10
|
||||||
|
auto_save_filters = true
|
||||||
|
default_sort = "added_desc" # added_desc, published_desc, title_asc
|
||||||
|
enable_geographic_hierarchy = true
|
||||||
|
auto_migrate_country_filters = true
|
||||||
|
|
||||||
|
[sharing]
|
||||||
|
default_format = "text"
|
||||||
|
include_summary = true
|
||||||
|
include_tags = true
|
||||||
|
include_source = true
|
||||||
|
copy_to_clipboard = true
|
||||||
|
|
||||||
|
[sharing.templates.text]
|
||||||
|
format = """
|
||||||
|
📰 {title}
|
||||||
|
|
||||||
|
{summary}
|
||||||
|
|
||||||
|
🏷️ Tags: {tags}
|
||||||
|
🌍 Location: {geographic_tags}
|
||||||
|
🔗 Source: {url}
|
||||||
|
📅 Published: {published_at}
|
||||||
|
|
||||||
|
Shared via Owly News Summariser
|
||||||
|
"""
|
||||||
|
|
||||||
|
[sharing.templates.markdown]
|
||||||
|
format = """
|
||||||
|
# {title}
|
||||||
|
|
||||||
|
{summary}
|
||||||
|
|
||||||
|
**Tags:** {tags}
|
||||||
|
**Location:** {geographic_tags}
|
||||||
|
**Source:** [{url}]({url})
|
||||||
|
**Published:** {published_at}
|
||||||
|
|
||||||
|
---
|
||||||
|
*Shared via Owly News Summariser*
|
||||||
|
"""
|
||||||
|
|
||||||
|
[ai]
|
||||||
|
enabled = true
|
||||||
|
provider = "ollama"
|
||||||
|
timeout_seconds = 120
|
||||||
|
|
||||||
|
[ai.summary]
|
||||||
|
enabled = true
|
||||||
|
temperature = 0.1
|
||||||
|
max_tokens = 1000
|
||||||
|
|
||||||
|
[ai.tagging]
|
||||||
|
enabled = true
|
||||||
|
temperature = 0.3
|
||||||
|
max_tokens = 200
|
||||||
|
max_tags_per_article = 10
|
||||||
|
min_confidence_threshold = 0.7
|
||||||
|
enable_geographic_tagging = true
|
||||||
|
enable_category_tagging = true
|
||||||
|
geographic_hierarchy_levels = 3 # country, region, city
|
||||||
|
|
||||||
|
[scraping]
|
||||||
|
timeout_seconds = 30
|
||||||
|
max_retries = 3
|
||||||
|
max_content_length = 50000
|
||||||
|
respect_robots_txt = true
|
||||||
|
rate_limit_delay_ms = 1000
|
||||||
|
|
||||||
|
[processing]
|
||||||
|
batch_size = 10
|
||||||
|
max_concurrent = 5
|
||||||
|
retry_attempts = 3
|
||||||
|
priority_manual = true
|
||||||
|
auto_mark_read_on_view = false
|
||||||
|
|
||||||
|
[migration]
|
||||||
|
auto_convert_country_filters = true
|
||||||
|
preserve_legacy_data = true
|
||||||
|
migration_batch_size = 100
|
||||||
|
|
||||||
|
[cli]
|
||||||
|
default_output = "table"
|
||||||
|
pager_command = "less"
|
||||||
|
show_progress = true
|
||||||
|
auto_confirm_bulk = false
|
||||||
|
show_geographic_hierarchy = true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Strategy for Country-Based Filtering
|
||||||
|
|
||||||
|
### Automatic Migration Process
|
||||||
|
1. **Data Analysis**: Scan existing country filter data and RSS feed origins
|
||||||
|
2. **Tag Generation**: Create geographic tags for each country with hierarchical structure
|
||||||
|
3. **Filter Conversion**: Convert country-based filters to tag-based equivalents
|
||||||
|
4. **User Notification**: Inform users about the migration and new capabilities
|
||||||
|
5. **Gradual Rollout**: Maintain backward compatibility during transition period
|
||||||
|
|
||||||
|
### Enhanced Geographic Features
|
||||||
|
- **Hierarchical Display**: Country → Region → City tag hierarchy
|
||||||
|
- **Visual Map Integration**: Interactive geographic filtering via map interface
|
||||||
|
- **Smart Suggestions**: Related location and content suggestions
|
||||||
|
- **Multi-Level Filtering**: Filter by specific cities, regions, or broader geographic areas
|
||||||
|
- **Source Intelligence**: AI detection of article geographic relevance beyond RSS origin
|
||||||
|
|
||||||
|
## Future Enhancements (Post Phase 3)
|
||||||
|
|
||||||
|
### Advanced Analytics
|
||||||
|
- **Machine Learning Insights**: Content recommendation based on reading patterns and geographic preferences
|
||||||
|
- **Predictive Analytics**: Trending topic prediction with geographic context
|
||||||
|
- **Behavioral Analysis**: Reading habit optimization suggestions
|
||||||
|
- **Comparative Analytics**: Benchmark against reading goals and regional averages
|
||||||
|
|
||||||
|
### Enhanced Content Management
|
||||||
|
- **Smart Collections**: AI-curated article collections with geographic themes
|
||||||
|
- **Reading Lists**: Planned reading with progress tracking
|
||||||
|
- **Content Relationships**: Related article suggestions with geographic relevance
|
||||||
|
- **Advanced Search**: Full-text search with relevance scoring and geographic weighting
|
||||||
|
|
||||||
|
### Social & Collaboration Features
|
||||||
|
- **Reading Groups**: Shared reading lists and discussions with geographic focus
|
||||||
|
- **Social Sharing**: Integration with social platforms
|
||||||
|
- **Collaborative Tagging**: Community-driven content organization
|
||||||
|
- **Reading Challenges**: Gamification of reading habits with geographic themes
|
||||||
|
|
||||||
|
### Integration & Extensibility
|
||||||
|
- **Browser Extension**: Seamless article saving and reading
|
||||||
|
- **Mobile Apps**: Native iOS/Android applications with location awareness
|
||||||
|
- **API Ecosystem**: Third-party integrations and plugins
|
||||||
|
- **Webhook System**: Real-time notifications and integrations with geographic filtering
|
||||||
|
72
backend-rust/TODO.md
Normal file
72
backend-rust/TODO.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
## CPU and resource limiting
|
||||||
|
- Tokio worker threads
|
||||||
|
- Decide thread policy:
|
||||||
|
- Option A: set TOKIO_WORKER_THREADS in the environment for deployments.
|
||||||
|
- Option B: build a custom runtime with tokio::runtime::Builder::new_multi_thread().worker_threads(n).
|
||||||
|
|
||||||
|
- Document your default policy (e.g., 50% of physical cores).
|
||||||
|
|
||||||
|
- Concurrency guard for CPU-heavy tasks
|
||||||
|
- Create a global tokio::sync::Semaphore with N permits (N = allowed concurrent heavy tasks).
|
||||||
|
- Acquire a permit before invoking heavy module operations; release automatically on drop.
|
||||||
|
- Expose the semaphore in app state so handlers/jobs can share it.
|
||||||
|
|
||||||
|
- HTTP backpressure and rate limiting (if using API)
|
||||||
|
- Add tower::limit::ConcurrencyLimitLayer to cap in-flight requests.
|
||||||
|
- Add tower::limit::RateLimitLayer or request-size/timeouts as needed.
|
||||||
|
- Optionally add tower::timeout::TimeoutLayer to bound handler latency.
|
||||||
|
|
||||||
|
- Stronger isolation (optional, later)
|
||||||
|
- Evaluate running certain modules as separate processes for strict CPU caps.
|
||||||
|
- Use cgroups v2 (Linux) or Job Objects (Windows) to bound CPU/memory per process.
|
||||||
|
- Reuse the same JSON interface over IPC (e.g., stdio or a local socket).
|
||||||
|
|
||||||
|
## Build and run
|
||||||
|
- Build all crates
|
||||||
|
- Run: cargo build --workspace
|
||||||
|
|
||||||
|
- Build each plugin as cdylib
|
||||||
|
- Example: cd crates/modules/summarizer && cargo build --release
|
||||||
|
|
||||||
|
- Stage plugin libraries for the host to find
|
||||||
|
- Create a modules directory the daemon will read, e.g. target/modules
|
||||||
|
- Copy the built artifact into that directory:
|
||||||
|
- Linux: copy target/release/libsummarizer.so -> target/modules/libsummarizer.so
|
||||||
|
- macOS: copy target/release/libsummarizer.dylib -> target/modules/libsummarizer.dylib
|
||||||
|
- Windows: copy target/release/summarizer.dll -> target/modules/summarizer.dll
|
||||||
|
|
||||||
|
- Alternatively set OWLY_MODULES_DIR to your chosen directory.
|
||||||
|
|
||||||
|
- Run the daemon
|
||||||
|
- cargo run -p owly-news
|
||||||
|
- Optionally set:
|
||||||
|
- OWLY_MODULES_DIR=/absolute/path/to/modules
|
||||||
|
- TOKIO_WORKER_THREADS=N
|
||||||
|
|
||||||
|
## Wire into the API
|
||||||
|
- Share ModuleHost in app state
|
||||||
|
- Create a struct AppState { host: Arc, cpu_sem: Arc , ... }.
|
||||||
|
- Add AppState to Axum with .with_state(state).
|
||||||
|
|
||||||
|
- In a handler (example: POST /summarize)
|
||||||
|
- Parse payload as JSON.
|
||||||
|
- Acquire a permit from cpu_sem before heavy work.
|
||||||
|
- host.get("summarizer").await? to lazily load the module.
|
||||||
|
- Call module.invoke_json("summarize", payload_value)?.
|
||||||
|
- Map success to 200 with JSON; map errors to appropriate status codes.
|
||||||
|
|
||||||
|
- Error handling and observability
|
||||||
|
- Use thiserror/anyhow to classify operational vs. client errors.
|
||||||
|
- Add tracing spans around module loading and invocation; include module name and op.
|
||||||
|
- Return structured error JSON when module reports an error.
|
||||||
|
|
||||||
|
- Configuration
|
||||||
|
- Decide env vars and defaults: OWLY_MODULES_DIR, TOKIO_WORKER_THREADS, concurrency permits, rate limits.
|
||||||
|
- Optionally add a config file (toml) and load via figment or config crate.
|
||||||
|
|
||||||
|
- Health and lifecycle
|
||||||
|
- Add a /health route that checks:
|
||||||
|
- Tokio is responsive.
|
||||||
|
- Optional: preflight-check that required modules are present (or skip to keep lazy).
|
||||||
|
|
||||||
|
- Graceful shutdown: listen for SIGINT/SIGTERM and drain in-flight requests before exit.
|
@@ -1,3 +0,0 @@
|
|||||||
[server]
|
|
||||||
host = '127.0.0.1'
|
|
||||||
port = 8090
|
|
7
backend-rust/crates/api/Cargo.lock
generated
Normal file
7
backend-rust/crates/api/Cargo.lock
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "api"
|
||||||
|
version = "0.1.0"
|
18
backend-rust/crates/api/Cargo.toml
Normal file
18
backend-rust/crates/api/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "api"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
once_cell = { workspace = true }
|
||||||
|
toml = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
async-trait = "0.1.89"
|
||||||
|
axum = { workspace = true }
|
||||||
|
sqlx = { workspace = true, features = ["sqlite"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
@@ -22,11 +22,13 @@ pub struct AppError(anyhow::Error);
|
|||||||
|
|
||||||
impl IntoResponse for AppError {
|
impl IntoResponse for AppError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
(
|
let (status, message) = match self.0.downcast_ref::<sqlx::Error>() {
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
Some(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Database error occurred"),
|
||||||
format!("Something went wrong: {}", self.0),
|
None => (StatusCode::INTERNAL_SERVER_ERROR, "An error occurred"),
|
||||||
)
|
};
|
||||||
.into_response()
|
|
||||||
|
tracing::error!("API Error: {:?}", self.0);
|
||||||
|
(status, message).into_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1003
backend-rust/crates/api/src/config.rs
Normal file
1003
backend-rust/crates/api/src/config.rs
Normal file
File diff suppressed because it is too large
Load Diff
6
backend-rust/crates/api/src/lib.rs
Normal file
6
backend-rust/crates/api/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
//! API-first core: shared types, DTOs, service traits, configuration.
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
|
pub mod types;
|
||||||
|
pub mod services;
|
||||||
|
pub mod api;
|
28
backend-rust/crates/api/src/services.rs
Normal file
28
backend-rust/crates/api/src/services.rs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
use crate::types::Health;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
// Submodules that host various domain services. These were refactored from the
|
||||||
|
// legacy root src folder into this workspace crate. Each component is its own module file.
|
||||||
|
pub mod summary_service;
|
||||||
|
pub mod news_service;
|
||||||
|
pub mod scraping_service;
|
||||||
|
pub mod tagging_service;
|
||||||
|
pub mod analytics_service;
|
||||||
|
pub mod sharing_service;
|
||||||
|
pub(crate) mod content_processor;
|
||||||
|
|
||||||
|
// Implement your service traits here. Example:
|
||||||
|
#[async_trait]
|
||||||
|
pub trait HealthService: Send + Sync {
|
||||||
|
async fn health(&self) -> Health;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A trivial default implementation that can be used by server and tests.
|
||||||
|
pub struct DefaultHealthService;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl HealthService for DefaultHealthService {
|
||||||
|
async fn health(&self) -> Health {
|
||||||
|
Health { status: "ok".into() }
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,4 @@
|
|||||||
|
//! Analytics service module.
|
||||||
|
//! Implement logic for tracking and aggregating analytics here.
|
||||||
|
|
||||||
|
// Placeholder for analytics-related types and functions.
|
@@ -0,0 +1,3 @@
|
|||||||
|
//! Content processor utilities shared by services.
|
||||||
|
|
||||||
|
// Placeholder module for content processing helpers (e.g., cleaning, tokenization).
|
4
backend-rust/crates/api/src/services/news_service.rs
Normal file
4
backend-rust/crates/api/src/services/news_service.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
//! News service module.
|
||||||
|
//! Implement logic related to news retrieval/management here.
|
||||||
|
|
||||||
|
// Placeholder for news-related types and functions.
|
4
backend-rust/crates/api/src/services/scraping_service.rs
Normal file
4
backend-rust/crates/api/src/services/scraping_service.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
//! Scraping service module.
|
||||||
|
//! Implement logic related to web scraping, fetchers, and extractors here.
|
||||||
|
|
||||||
|
// Placeholder for scraping-related types and functions.
|
4
backend-rust/crates/api/src/services/sharing_service.rs
Normal file
4
backend-rust/crates/api/src/services/sharing_service.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
//! Sharing service module.
|
||||||
|
//! Implement logic related to content sharing here.
|
||||||
|
|
||||||
|
// Placeholder for sharing-related types and functions.
|
4
backend-rust/crates/api/src/services/summary_service.rs
Normal file
4
backend-rust/crates/api/src/services/summary_service.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
//! Summary service module.
|
||||||
|
//! Implement logic for generating summaries from articles here.
|
||||||
|
|
||||||
|
// Placeholder for summary-related types and functions.
|
4
backend-rust/crates/api/src/services/tagging_service.rs
Normal file
4
backend-rust/crates/api/src/services/tagging_service.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
//! Tagging service module.
|
||||||
|
//! Implement logic related to tagging articles and managing tags here.
|
||||||
|
|
||||||
|
// Placeholder for tagging-related types and functions.
|
6
backend-rust/crates/api/src/types.rs
Normal file
6
backend-rust/crates/api/src/types.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Health {
|
||||||
|
pub status: String,
|
||||||
|
}
|
15
backend-rust/crates/cli/Cargo.toml
Normal file
15
backend-rust/crates/cli/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
toml = { workspace = true }
|
||||||
|
dotenv = { workspace = true }
|
||||||
|
|
||||||
|
api = { path = "../api" }
|
||||||
|
server = { path = "../server" }
|
70
backend-rust/crates/cli/src/main.rs
Normal file
70
backend-rust/crates/cli/src/main.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use api::config::Cli;
|
||||||
|
use dotenv::dotenv;
|
||||||
|
use std::{env, net::SocketAddr, str::FromStr};
|
||||||
|
use tokio::signal;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
dotenv().ok();
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
|
||||||
|
match args.get(1).map(|s| s.as_str()) {
|
||||||
|
Some("serve") => serve(args).await,
|
||||||
|
Some("print-config") => print_config(),
|
||||||
|
_ => {
|
||||||
|
print_help();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_help() {
|
||||||
|
eprintln!(
|
||||||
|
"Usage:
|
||||||
|
cli serve [--addr 0.0.0.0:8080]
|
||||||
|
cli print-config
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
These may influence runtime behavior.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- 'serve' runs the HTTP server.
|
||||||
|
- 'print-config' prints the default CLI configuration in JSON."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn serve(args: Vec<String>) -> Result<()> {
|
||||||
|
// naive flag parse: look for "--addr host:port"
|
||||||
|
let mut addr: SocketAddr = SocketAddr::from_str("127.0.0.1:8080")?;
|
||||||
|
let mut i = 2;
|
||||||
|
while i + 1 < args.len() {
|
||||||
|
if args[i] == "--addr" {
|
||||||
|
addr = SocketAddr::from_str(&args[i + 1])?;
|
||||||
|
i += 2;
|
||||||
|
} else {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let server_task = tokio::spawn(async move { server::start_server(addr).await });
|
||||||
|
|
||||||
|
// graceful shutdown via Ctrl+C
|
||||||
|
tokio::select! {
|
||||||
|
res = server_task => {
|
||||||
|
res??;
|
||||||
|
}
|
||||||
|
_ = signal::ctrl_c() => {
|
||||||
|
eprintln!("Shutting down...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_config() -> Result<()> {
|
||||||
|
let cfg = Cli::default();
|
||||||
|
let json = serde_json::to_string_pretty(&cfg)?;
|
||||||
|
println!("{json}");
|
||||||
|
Ok(())
|
||||||
|
}
|
10
backend-rust/crates/db/Cargo.toml
Normal file
10
backend-rust/crates/db/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "db"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
sqlx = { workspace = true, features = ["sqlite"] }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
api = { path = "../api" }
|
44
backend-rust/crates/db/src/lib.rs
Normal file
44
backend-rust/crates/db/src/lib.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
use api::config::AppSettings;
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use sqlx::migrate::Migrator;
|
||||||
|
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
|
||||||
|
use sqlx::{Pool, Sqlite, SqlitePool};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
// Embed migrations from the workspace-level migrations directory.
|
||||||
|
// crates/db is two levels below backend-rust where migrations/ resides.
|
||||||
|
pub const MIGRATOR: Migrator = sqlx::migrate!("../../migrations");
|
||||||
|
|
||||||
|
pub async fn initialize_db(app_settings: &AppSettings) -> Result<Pool<Sqlite>> {
|
||||||
|
app_settings
|
||||||
|
.ensure_default_directory()
|
||||||
|
.context("Failed to ensure default directory for database")?;
|
||||||
|
|
||||||
|
let options = SqliteConnectOptions::from_str(&app_settings.database_url())?
|
||||||
|
.create_if_missing(true)
|
||||||
|
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
||||||
|
.foreign_keys(true);
|
||||||
|
|
||||||
|
let pool = SqlitePoolOptions::new()
|
||||||
|
.max_connections(20)
|
||||||
|
.min_connections(5)
|
||||||
|
.acquire_timeout(Duration::from_secs(30))
|
||||||
|
.idle_timeout(Duration::from_secs(600))
|
||||||
|
.connect_with(options)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
MIGRATOR
|
||||||
|
.run(&pool)
|
||||||
|
.await
|
||||||
|
.with_context(|| "Database migrations failed")?;
|
||||||
|
info!("Database migrations completed successfully");
|
||||||
|
|
||||||
|
Ok(pool)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_pool(opts: SqliteConnectOptions) -> Result<SqlitePool> {
|
||||||
|
let pool = SqlitePool::connect_with(opts).await?;
|
||||||
|
Ok(pool)
|
||||||
|
}
|
23
backend-rust/crates/server/Cargo.toml
Normal file
23
backend-rust/crates/server/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
[package]
|
||||||
|
name = "server"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
tracing-subscriber = { workspace = true }
|
||||||
|
axum = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
sqlx = { workspace = true, features = ["sqlite"] }
|
||||||
|
dotenv = { workspace = true }
|
||||||
|
once_cell = { workspace = true }
|
||||||
|
|
||||||
|
api = { path = "../api" }
|
||||||
|
db = { path = "../db" }
|
||||||
|
http = "1.3.1"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
63
backend-rust/crates/server/src/lib.rs
Normal file
63
backend-rust/crates/server/src/lib.rs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
use axum::{routing::get, Json, Router};
|
||||||
|
use std::{net::SocketAddr, sync::Arc};
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use tracing::{info, level_filters::LevelFilter};
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
use api::services::{DefaultHealthService, HealthService};
|
||||||
|
use api::types::Health;
|
||||||
|
use api::config::AppSettings;
|
||||||
|
|
||||||
|
pub struct AppState {
|
||||||
|
pub health_service: Arc<dyn HealthService>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn build_router(state: Arc<AppState>) -> Router {
|
||||||
|
Router::new().route(
|
||||||
|
"/health",
|
||||||
|
get({
|
||||||
|
let state = state.clone();
|
||||||
|
move || health_handler(state.clone())
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health_handler(state: Arc<AppState>) -> Json<Health> {
|
||||||
|
let res = state.health_service.health().await;
|
||||||
|
Json(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_server(addr: SocketAddr) -> anyhow::Result<()> {
|
||||||
|
init_tracing();
|
||||||
|
|
||||||
|
// Load application settings and initialize the database pool (sqlite).
|
||||||
|
let app_settings = AppSettings::get_app_settings();
|
||||||
|
let pool = db::initialize_db(&app_settings).await?;
|
||||||
|
|
||||||
|
let state = Arc::new(AppState {
|
||||||
|
health_service: Arc::new(DefaultHealthService),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Base daemon router
|
||||||
|
let app = build_router(state).await
|
||||||
|
// Attach API under /api and provide DB state
|
||||||
|
.nest("/api", api::api::routes::routes().with_state(pool.clone()));
|
||||||
|
|
||||||
|
let listener = TcpListener::bind(addr).await?;
|
||||||
|
info!("HTTP server listening on http://{}", addr);
|
||||||
|
axum::serve(listener, app).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_tracing() {
|
||||||
|
let env_filter = EnvFilter::try_from_default_env()
|
||||||
|
.or_else(|_| EnvFilter::try_new("info"))
|
||||||
|
.unwrap()
|
||||||
|
.add_directive(LevelFilter::INFO.into());
|
||||||
|
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(env_filter)
|
||||||
|
.with_target(true)
|
||||||
|
.compact()
|
||||||
|
.init();
|
||||||
|
}
|
22
backend-rust/crates/server/tests/health.rs
Normal file
22
backend-rust/crates/server/tests/health.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
use axum::Router;
|
||||||
|
use server::{build_router, AppState};
|
||||||
|
use api::services::DefaultHealthService;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn health_ok() {
|
||||||
|
let state = Arc::new(AppState {
|
||||||
|
health_service: Arc::new(DefaultHealthService),
|
||||||
|
});
|
||||||
|
|
||||||
|
let app: Router = build_router(state).await;
|
||||||
|
|
||||||
|
let req = http::Request::builder()
|
||||||
|
.uri("/health")
|
||||||
|
.body(axum::body::Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let res = axum::http::Request::from(req);
|
||||||
|
let res = axum::http::Request::from(res);
|
||||||
|
let _ = app; // You can use axum-test to send requests if desired.
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
-- Drop articles table and its indexes
|
||||||
|
DROP INDEX IF EXISTS idx_articles_read_at;
|
||||||
|
DROP INDEX IF EXISTS idx_articles_source_type;
|
||||||
|
DROP INDEX IF EXISTS idx_articles_processing_status;
|
||||||
|
DROP INDEX IF EXISTS idx_articles_added_at;
|
||||||
|
DROP INDEX IF EXISTS idx_articles_published_at;
|
||||||
|
DROP TABLE IF EXISTS articles;
|
27
backend-rust/migrations/003_create_articles_table.up.sql
Normal file
27
backend-rust/migrations/003_create_articles_table.up.sql
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
-- Create enhanced articles table to replace news table structure
|
||||||
|
CREATE TABLE IF NOT EXISTS articles
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
source_type TEXT NOT NULL DEFAULT 'rss', -- 'rss', 'manual'
|
||||||
|
rss_content TEXT, -- RSS description/excerpt
|
||||||
|
full_content TEXT, -- Scraped full content
|
||||||
|
summary TEXT, -- AI-generated summary
|
||||||
|
processing_status TEXT NOT NULL DEFAULT 'pending', -- 'pending', 'processing', 'completed', 'failed'
|
||||||
|
published_at TIMESTAMP NOT NULL,
|
||||||
|
added_at TIMESTAMP NOT NULL DEFAULT (datetime('now')),
|
||||||
|
read_at TIMESTAMP,
|
||||||
|
read_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
reading_time INTEGER, -- in seconds
|
||||||
|
ai_enabled BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create indexes for performance
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_articles_published_at ON articles (published_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_articles_added_at ON articles (added_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_articles_processing_status ON articles (processing_status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_articles_source_type ON articles (source_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_articles_read_at ON articles (read_at);
|
9
backend-rust/migrations/004_create_tags_table.down.sql
Normal file
9
backend-rust/migrations/004_create_tags_table.down.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
-- Drop tag system tables and indexes
|
||||||
|
DROP INDEX IF EXISTS idx_article_tags_ai_generated;
|
||||||
|
DROP INDEX IF EXISTS idx_article_tags_tag_id;
|
||||||
|
DROP INDEX IF EXISTS idx_article_tags_article_id;
|
||||||
|
DROP INDEX IF EXISTS idx_tags_usage_count;
|
||||||
|
DROP INDEX IF EXISTS idx_tags_parent_id;
|
||||||
|
DROP INDEX IF EXISTS idx_tags_category;
|
||||||
|
DROP TABLE IF EXISTS article_tags;
|
||||||
|
DROP TABLE IF EXISTS tags;
|
31
backend-rust/migrations/004_create_tags_table.up.sql
Normal file
31
backend-rust/migrations/004_create_tags_table.up.sql
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
-- Create tags table with hierarchical support
|
||||||
|
CREATE TABLE IF NOT EXISTS tags
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
category TEXT NOT NULL, -- 'geographic', 'content', 'source', 'custom'
|
||||||
|
description TEXT,
|
||||||
|
color TEXT, -- Hex color for UI display
|
||||||
|
usage_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
parent_id INTEGER REFERENCES tags (id), -- For hierarchical tags (e.g., Country -> Region -> City)
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create article_tags junction table
|
||||||
|
CREATE TABLE IF NOT EXISTS article_tags
|
||||||
|
(
|
||||||
|
article_id INTEGER NOT NULL REFERENCES articles (id) ON DELETE CASCADE,
|
||||||
|
tag_id INTEGER NOT NULL REFERENCES tags (id) ON DELETE CASCADE,
|
||||||
|
confidence_score REAL DEFAULT 1.0, -- AI confidence (0.0-1.0)
|
||||||
|
ai_generated BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT (datetime('now')),
|
||||||
|
PRIMARY KEY (article_id, tag_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tags_category ON tags (category);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tags_parent_id ON tags (parent_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tags_usage_count ON tags (usage_count DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_article_tags_article_id ON article_tags (article_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_article_tags_tag_id ON article_tags (tag_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_article_tags_ai_generated ON article_tags (ai_generated);
|
11
backend-rust/migrations/005_create_statistics_table.down.sql
Normal file
11
backend-rust/migrations/005_create_statistics_table.down.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-- Drop analytics system tables and indexes
|
||||||
|
DROP INDEX IF EXISTS idx_legacy_migration_old_filter_type;
|
||||||
|
DROP INDEX IF EXISTS idx_share_templates_format;
|
||||||
|
DROP INDEX IF EXISTS idx_filter_presets_user_id;
|
||||||
|
DROP INDEX IF EXISTS idx_reading_stats_read_at;
|
||||||
|
DROP INDEX IF EXISTS idx_reading_stats_article_id;
|
||||||
|
DROP INDEX IF EXISTS idx_reading_stats_user_id;
|
||||||
|
DROP TABLE IF EXISTS legacy_migration;
|
||||||
|
DROP TABLE IF EXISTS share_templates;
|
||||||
|
DROP TABLE IF EXISTS filter_presets;
|
||||||
|
DROP TABLE IF EXISTS reading_stats;
|
50
backend-rust/migrations/005_create_statistics_table.up.sql
Normal file
50
backend-rust/migrations/005_create_statistics_table.up.sql
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
-- Create reading statistics table
|
||||||
|
CREATE TABLE IF NOT EXISTS reading_stats
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER DEFAULT 1, -- For future multi-user support
|
||||||
|
article_id INTEGER NOT NULL REFERENCES articles (id) ON DELETE CASCADE,
|
||||||
|
read_at TIMESTAMP NOT NULL,
|
||||||
|
reading_time INTEGER, -- in seconds
|
||||||
|
completion_rate REAL DEFAULT 1.0, -- 0.0-1.0, how much of the article was read
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create filter presets table
|
||||||
|
CREATE TABLE IF NOT EXISTS filter_presets
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
filter_criteria TEXT NOT NULL, -- JSON string of filter parameters
|
||||||
|
user_id INTEGER DEFAULT 1, -- For future multi-user support
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create share templates table
|
||||||
|
CREATE TABLE IF NOT EXISTS share_templates
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
format TEXT NOT NULL, -- 'text', 'markdown', 'html', 'json'
|
||||||
|
template_content TEXT NOT NULL,
|
||||||
|
is_default BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create legacy migration tracking table
|
||||||
|
CREATE TABLE IF NOT EXISTS legacy_migration
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
old_filter_type TEXT NOT NULL, -- 'country', 'category', etc.
|
||||||
|
old_value TEXT NOT NULL,
|
||||||
|
new_tag_ids TEXT, -- JSON array of tag IDs
|
||||||
|
migrated_at TIMESTAMP NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_reading_stats_user_id ON reading_stats (user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_reading_stats_article_id ON reading_stats (article_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_reading_stats_read_at ON reading_stats (read_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_filter_presets_user_id ON filter_presets (user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_share_templates_format ON share_templates (format);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_legacy_migration_old_filter_type ON legacy_migration (old_filter_type);
|
18
backend-rust/migrations/006_update_settings_table.down.sql
Normal file
18
backend-rust/migrations/006_update_settings_table.down.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
-- Remove enhanced settings columns and indexes
|
||||||
|
DROP INDEX IF EXISTS idx_settings_user_id;
|
||||||
|
DROP INDEX IF EXISTS idx_settings_category;
|
||||||
|
|
||||||
|
-- Note: SQLite doesn't support DROP COLUMN, so we recreate the table
|
||||||
|
CREATE TABLE settings_backup AS
|
||||||
|
SELECT key, val
|
||||||
|
FROM settings;
|
||||||
|
DROP TABLE settings;
|
||||||
|
CREATE TABLE settings
|
||||||
|
(
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
val TEXT NOT NULL
|
||||||
|
);
|
||||||
|
INSERT INTO settings
|
||||||
|
SELECT key, val
|
||||||
|
FROM settings_backup;
|
||||||
|
DROP TABLE settings_backup;
|
74
backend-rust/migrations/006_update_settings_table.up.sql
Normal file
74
backend-rust/migrations/006_update_settings_table.up.sql
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
-- Enhance settings table to support more structured configuration
|
||||||
|
ALTER TABLE settings
|
||||||
|
ADD COLUMN category TEXT DEFAULT 'general';
|
||||||
|
ALTER TABLE settings
|
||||||
|
ADD COLUMN user_id INTEGER DEFAULT 1;
|
||||||
|
ALTER TABLE settings
|
||||||
|
ADD COLUMN updated_at TIMESTAMP DEFAULT (datetime('now'));
|
||||||
|
|
||||||
|
-- Create index for better performance
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_settings_category ON settings (category);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_settings_user_id ON settings (user_id);
|
||||||
|
|
||||||
|
-- Insert default settings based on roadmap configuration
|
||||||
|
INSERT OR IGNORE INTO settings (key, val, category)
|
||||||
|
VALUES
|
||||||
|
-- Display settings
|
||||||
|
('default_view', 'compact', 'display'),
|
||||||
|
('articles_per_page', '50', 'display'),
|
||||||
|
('show_reading_time', '1', 'display'),
|
||||||
|
('show_word_count', '0', 'display'),
|
||||||
|
('highlight_unread', '1', 'display'),
|
||||||
|
('theme', 'auto', 'display'),
|
||||||
|
|
||||||
|
-- Analytics settings
|
||||||
|
('analytics_enabled', '1', 'analytics'),
|
||||||
|
('track_reading_time', '1', 'analytics'),
|
||||||
|
('track_scroll_position', '1', 'analytics'),
|
||||||
|
('retention_days', '365', 'analytics'),
|
||||||
|
('aggregate_older_data', '1', 'analytics'),
|
||||||
|
|
||||||
|
-- Filtering settings
|
||||||
|
('enable_smart_suggestions', '1', 'filtering'),
|
||||||
|
('max_recent_filters', '10', 'filtering'),
|
||||||
|
('auto_save_filters', '1', 'filtering'),
|
||||||
|
('default_sort', 'added_desc', 'filtering'),
|
||||||
|
('enable_geographic_hierarchy', '1', 'filtering'),
|
||||||
|
('auto_migrate_country_filters', '1', 'filtering'),
|
||||||
|
|
||||||
|
-- Sharing settings
|
||||||
|
('default_share_format', 'text', 'sharing'),
|
||||||
|
('include_summary', '1', 'sharing'),
|
||||||
|
('include_tags', '1', 'sharing'),
|
||||||
|
('include_source', '1', 'sharing'),
|
||||||
|
('copy_to_clipboard', '1', 'sharing'),
|
||||||
|
|
||||||
|
-- AI settings
|
||||||
|
('ai_enabled', '1', 'ai'),
|
||||||
|
('ai_provider', 'ollama', 'ai'),
|
||||||
|
('ai_timeout_seconds', '120', 'ai'),
|
||||||
|
('ai_summary_enabled', '1', 'ai'),
|
||||||
|
('ai_summary_temperature', '0.1', 'ai'),
|
||||||
|
('ai_summary_max_tokens', '1000', 'ai'),
|
||||||
|
('ai_tagging_enabled', '1', 'ai'),
|
||||||
|
('ai_tagging_temperature', '0.3', 'ai'),
|
||||||
|
('ai_tagging_max_tokens', '200', 'ai'),
|
||||||
|
('max_tags_per_article', '10', 'ai'),
|
||||||
|
('min_confidence_threshold', '0.7', 'ai'),
|
||||||
|
('enable_geographic_tagging', '1', 'ai'),
|
||||||
|
('enable_category_tagging', '1', 'ai'),
|
||||||
|
('geographic_hierarchy_levels', '3', 'ai'),
|
||||||
|
|
||||||
|
-- Scraping settings
|
||||||
|
('scraping_timeout_seconds', '30', 'scraping'),
|
||||||
|
('scraping_max_retries', '3', 'scraping'),
|
||||||
|
('max_content_length', '50000', 'scraping'),
|
||||||
|
('respect_robots_txt', '1', 'scraping'),
|
||||||
|
('rate_limit_delay_ms', '1000', 'scraping'),
|
||||||
|
|
||||||
|
-- Processing settings
|
||||||
|
('batch_size', '10', 'processing'),
|
||||||
|
('max_concurrent', '5', 'processing'),
|
||||||
|
('retry_attempts', '3', 'processing'),
|
||||||
|
('priority_manual', '1', 'processing'),
|
||||||
|
('auto_mark_read_on_view', '0', 'processing');
|
@@ -0,0 +1,39 @@
|
|||||||
|
-- Remove migrated data (this will remove all articles and tags created from migration)
|
||||||
|
-- WARNING: This will delete all migrated data
|
||||||
|
|
||||||
|
-- Remove legacy migration records
|
||||||
|
DELETE
|
||||||
|
FROM legacy_migration
|
||||||
|
WHERE old_filter_type IN ('country', 'category');
|
||||||
|
|
||||||
|
-- Remove article-tag associations for migrated data (non-AI generated)
|
||||||
|
DELETE
|
||||||
|
FROM article_tags
|
||||||
|
WHERE ai_generated = 0;
|
||||||
|
|
||||||
|
-- Remove migrated geographic tags (only those created from country data)
|
||||||
|
DELETE
|
||||||
|
FROM tags
|
||||||
|
WHERE tags.category = 'geographic'
|
||||||
|
AND EXISTS (SELECT 1 FROM news WHERE news.country = tags.name);
|
||||||
|
|
||||||
|
-- Remove migrated content tags (only those created from category data)
|
||||||
|
DELETE
|
||||||
|
FROM tags
|
||||||
|
WHERE tags.category = 'content'
|
||||||
|
AND EXISTS (SELECT 1 FROM news WHERE news.category = tags.name);
|
||||||
|
|
||||||
|
-- Remove migrated articles (only those that match news entries)
|
||||||
|
DELETE
|
||||||
|
FROM articles
|
||||||
|
WHERE EXISTS (SELECT 1
|
||||||
|
FROM news
|
||||||
|
WHERE news.url = articles.url
|
||||||
|
AND news.title = articles.title
|
||||||
|
AND articles.source_type = 'rss');
|
||||||
|
|
||||||
|
-- Reset tag usage counts
|
||||||
|
UPDATE tags
|
||||||
|
SET usage_count = (SELECT COUNT(*)
|
||||||
|
FROM article_tags
|
||||||
|
WHERE tag_id = tags.id);
|
@@ -0,0 +1,84 @@
|
|||||||
|
|
||||||
|
-- Migrate data from old news table to new articles table
|
||||||
|
INSERT INTO articles (title, url, summary, published_at, added_at, source_type, processing_status)
|
||||||
|
SELECT title,
|
||||||
|
url,
|
||||||
|
summary,
|
||||||
|
published,
|
||||||
|
datetime(created_at, 'unixepoch'),
|
||||||
|
'rss',
|
||||||
|
CASE
|
||||||
|
WHEN summary IS NOT NULL AND summary != '' THEN 'completed'
|
||||||
|
ELSE 'pending'
|
||||||
|
END
|
||||||
|
FROM news;
|
||||||
|
|
||||||
|
-- Create geographic tags from existing country data
|
||||||
|
INSERT OR IGNORE INTO tags (name, category, description, usage_count)
|
||||||
|
SELECT DISTINCT country,
|
||||||
|
'geographic',
|
||||||
|
'Geographic location: ' || country,
|
||||||
|
COUNT(*)
|
||||||
|
FROM news
|
||||||
|
WHERE country IS NOT NULL
|
||||||
|
AND country != ''
|
||||||
|
GROUP BY country;
|
||||||
|
|
||||||
|
-- Link articles to their geographic tags
|
||||||
|
INSERT OR IGNORE INTO article_tags (article_id, tag_id, ai_generated, confidence_score)
|
||||||
|
SELECT a.id,
|
||||||
|
t.id,
|
||||||
|
0, -- Not AI generated, migrated from legacy data
|
||||||
|
1.0 -- Full confidence for existing data
|
||||||
|
FROM articles a
|
||||||
|
JOIN news n ON a.url = n.url AND a.title = n.title
|
||||||
|
JOIN tags t ON t.name = n.country AND t.category = 'geographic'
|
||||||
|
WHERE n.country IS NOT NULL
|
||||||
|
AND n.country != '';
|
||||||
|
|
||||||
|
-- Create category tags if category column exists in news table
|
||||||
|
INSERT OR IGNORE INTO tags (name, category, description, usage_count)
|
||||||
|
SELECT DISTINCT n.category,
|
||||||
|
'content',
|
||||||
|
'Content category: ' || n.category,
|
||||||
|
COUNT(*)
|
||||||
|
FROM news n
|
||||||
|
WHERE n.category IS NOT NULL
|
||||||
|
AND n.category != ''
|
||||||
|
GROUP BY n.category;
|
||||||
|
|
||||||
|
-- Link articles to their category tags
|
||||||
|
INSERT OR IGNORE INTO article_tags (article_id, tag_id, ai_generated, confidence_score)
|
||||||
|
SELECT a.id,
|
||||||
|
t.id,
|
||||||
|
0, -- Not AI generated, migrated from legacy data
|
||||||
|
1.0 -- Full confidence for existing data
|
||||||
|
FROM articles a
|
||||||
|
JOIN news n ON a.url = n.url AND a.title = n.title
|
||||||
|
JOIN tags t ON t.name = n.category AND t.category = 'content'
|
||||||
|
WHERE n.category IS NOT NULL
|
||||||
|
AND n.category != '';
|
||||||
|
|
||||||
|
-- Record migration in legacy_migration table for countries
|
||||||
|
INSERT INTO legacy_migration (old_filter_type, old_value, new_tag_ids)
|
||||||
|
SELECT 'country',
|
||||||
|
n.country,
|
||||||
|
'[' || GROUP_CONCAT(t.id) || ']'
|
||||||
|
FROM (SELECT DISTINCT country FROM news WHERE country IS NOT NULL AND country != '') n
|
||||||
|
JOIN tags t ON t.name = n.country AND t.category = 'geographic'
|
||||||
|
GROUP BY n.country;
|
||||||
|
|
||||||
|
-- Record migration in legacy_migration table for categories (if they exist)
|
||||||
|
INSERT INTO legacy_migration (old_filter_type, old_value, new_tag_ids)
|
||||||
|
SELECT 'category',
|
||||||
|
n.category,
|
||||||
|
'[' || GROUP_CONCAT(t.id) || ']'
|
||||||
|
FROM (SELECT DISTINCT category FROM news WHERE category IS NOT NULL AND category != '') n
|
||||||
|
JOIN tags t ON t.name = n.category AND t.category = 'content'
|
||||||
|
GROUP BY n.category;
|
||||||
|
|
||||||
|
-- Update tag usage counts
|
||||||
|
UPDATE tags
|
||||||
|
SET usage_count = (SELECT COUNT(*)
|
||||||
|
FROM article_tags
|
||||||
|
WHERE tag_id = tags.id);
|
@@ -0,0 +1,4 @@
|
|||||||
|
-- Remove default sharing templates
|
||||||
|
DELETE
|
||||||
|
FROM share_templates
|
||||||
|
WHERE name IN ('Default Text', 'Markdown', 'Simple Text', 'HTML Email');
|
@@ -0,0 +1,39 @@
|
|||||||
|
-- Insert default sharing templates
|
||||||
|
INSERT INTO share_templates (name, format, template_content, is_default)
|
||||||
|
VALUES ('Default Text', 'text', '📰 {title}
|
||||||
|
|
||||||
|
{summary}
|
||||||
|
|
||||||
|
🏷️ Tags: {tags}
|
||||||
|
🌍 Location: {geographic_tags}
|
||||||
|
🔗 Source: {url}
|
||||||
|
📅 Published: {published_at}
|
||||||
|
|
||||||
|
Shared via Owly News Summariser', 1),
|
||||||
|
|
||||||
|
('Markdown', 'markdown', '# {title}
|
||||||
|
|
||||||
|
{summary}
|
||||||
|
|
||||||
|
**Tags:** {tags}
|
||||||
|
**Location:** {geographic_tags}
|
||||||
|
**Source:** [{url}]({url})
|
||||||
|
**Published:** {published_at}
|
||||||
|
|
||||||
|
---
|
||||||
|
*Shared via Owly News Summariser*', 1),
|
||||||
|
|
||||||
|
('Simple Text', 'text', '{title}
|
||||||
|
|
||||||
|
{summary}
|
||||||
|
|
||||||
|
Source: {url}', 0),
|
||||||
|
|
||||||
|
('HTML Email', 'html', '<h2>{title}</h2>
|
||||||
|
<p>{summary}</p>
|
||||||
|
<p><strong>Tags:</strong> {tags}<br>
|
||||||
|
<strong>Location:</strong> {geographic_tags}<br>
|
||||||
|
<strong>Source:</strong> <a href="{url}">{url}</a><br>
|
||||||
|
<strong>Published:</strong> {published_at}</p>
|
||||||
|
<hr>
|
||||||
|
<small>Shared via Owly News Summariser</small>', 0);
|
@@ -1,100 +0,0 @@
|
|||||||
use serde::Deserialize;
|
|
||||||
use std::env;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use toml::Value;
|
|
||||||
use tracing::{error, info};
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
pub struct AppSettings {
|
|
||||||
pub config_path: String,
|
|
||||||
pub db_path: String,
|
|
||||||
pub migration_path: String,
|
|
||||||
pub config: Config,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
pub struct Config {
|
|
||||||
pub server: Server,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
pub struct Server {
|
|
||||||
pub host: String,
|
|
||||||
pub port: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct ConfigFile {
|
|
||||||
server: Server,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppSettings {
|
|
||||||
pub fn get_app_settings() -> Self {
|
|
||||||
let config_file = Self::load_config_file().unwrap_or_else(|| {
|
|
||||||
info!("Using default config values");
|
|
||||||
ConfigFile {
|
|
||||||
server: Server {
|
|
||||||
host: "127.0.0.1".to_string(),
|
|
||||||
port: 1337,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
config_path: Self::get_config_path(),
|
|
||||||
db_path: Self::get_db_path(),
|
|
||||||
migration_path: String::from("./migrations"),
|
|
||||||
config: Config {
|
|
||||||
server: config_file.server,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_config_file() -> Option<ConfigFile> {
|
|
||||||
let config_path = Self::get_config_path();
|
|
||||||
let contents = std::fs::read_to_string(&config_path)
|
|
||||||
.map_err(|e| error!("Failed to read config file: {}", e))
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
toml::from_str(&contents)
|
|
||||||
.map_err(|e| error!("Failed to parse TOML: {}", e))
|
|
||||||
.ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_db_path() -> String {
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
// Development: Use backend-rust directory
|
|
||||||
// TODO: Change later
|
|
||||||
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
||||||
path.push("owlynews.sqlite3");
|
|
||||||
path.to_str().unwrap().to_string()
|
|
||||||
} else {
|
|
||||||
// Production: Use standard Linux applications data directory
|
|
||||||
"/var/lib/owly-news-summariser/owlynews.sqlite3".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_config_path() -> String {
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
// Development: Use backend-rust directory
|
|
||||||
// TODO: Change later
|
|
||||||
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
||||||
path.push("config.toml");
|
|
||||||
path.to_str().unwrap().to_string()
|
|
||||||
} else {
|
|
||||||
// Production: Use standard Linux applications data directory
|
|
||||||
"$HOME/owly-news-summariser/config.toml".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn database_url(&self) -> String {
|
|
||||||
format!("sqlite:{}", self.db_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ensure_db_directory(&self) -> Result<(), std::io::Error> {
|
|
||||||
if let Some(parent) = std::path::Path::new(&self.db_path).parent() {
|
|
||||||
std::fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,31 +0,0 @@
|
|||||||
use crate::config::{AppSettings};
|
|
||||||
use anyhow::Result;
|
|
||||||
use sqlx::migrate::Migrator;
|
|
||||||
use sqlx::sqlite::{SqliteConnectOptions};
|
|
||||||
use sqlx::{Pool, Sqlite, SqlitePool};
|
|
||||||
use std::str::FromStr;
|
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
pub const MIGRATOR: Migrator = sqlx::migrate!("./migrations");
|
|
||||||
|
|
||||||
pub async fn initialize_db(app_settings: &AppSettings) -> Result<Pool<Sqlite>> {
|
|
||||||
app_settings.ensure_db_directory()?;
|
|
||||||
|
|
||||||
let options = SqliteConnectOptions::from_str(&app_settings.database_url())?
|
|
||||||
.create_if_missing(true)
|
|
||||||
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
|
||||||
.foreign_keys(true);
|
|
||||||
|
|
||||||
let pool = SqlitePool::connect_with(options).await?;
|
|
||||||
|
|
||||||
MIGRATOR.run(&pool).await?;
|
|
||||||
info!("Database migrations completed successfully");
|
|
||||||
|
|
||||||
Ok(pool)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_pool(opts: SqliteConnectOptions) -> Result<SqlitePool> {
|
|
||||||
let pool = SqlitePool::connect_with(opts).await?;
|
|
||||||
|
|
||||||
Ok(pool)
|
|
||||||
}
|
|
@@ -1,74 +0,0 @@
|
|||||||
mod api;
|
|
||||||
mod config;
|
|
||||||
mod db;
|
|
||||||
mod models;
|
|
||||||
mod services;
|
|
||||||
|
|
||||||
use crate::config::{AppSettings};
|
|
||||||
use anyhow::Result;
|
|
||||||
use axum::Router;
|
|
||||||
use axum::routing::get;
|
|
||||||
use tokio::signal;
|
|
||||||
use tracing::{info};
|
|
||||||
use tracing_subscriber;
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> Result<()> {
|
|
||||||
tracing_subscriber::fmt()
|
|
||||||
.with_target(false)
|
|
||||||
.compact()
|
|
||||||
.init();
|
|
||||||
|
|
||||||
let app_settings = AppSettings::get_app_settings();
|
|
||||||
|
|
||||||
let pool = db::initialize_db(&app_settings).await?;
|
|
||||||
|
|
||||||
let app = create_app(pool);
|
|
||||||
|
|
||||||
let listener =
|
|
||||||
tokio::net::TcpListener::bind(format!("{}:{}", app_settings.config.server.host, app_settings.config.server.port)).await?;
|
|
||||||
info!("Server starting on {}:{}", app_settings.config.server.host, app_settings.config.server.port);
|
|
||||||
|
|
||||||
axum::serve(listener, app)
|
|
||||||
.with_graceful_shutdown(shutdown_signal())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_app(pool: sqlx::SqlitePool) -> Router {
|
|
||||||
Router::new()
|
|
||||||
.route("/health", get(health_check))
|
|
||||||
.nest("/api", api::routes::routes())
|
|
||||||
.with_state(pool)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn health_check() -> &'static str {
|
|
||||||
"OK"
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn shutdown_signal() {
|
|
||||||
let ctrl_c = async {
|
|
||||||
signal::ctrl_c()
|
|
||||||
.await
|
|
||||||
.expect("failed to install CTRL+C handler");
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
let terminate = async {
|
|
||||||
signal::unix::signal(signal::unix::SignalKind::terminate())
|
|
||||||
.expect("failed to install terminate handler")
|
|
||||||
.recv()
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
let terminate = std::future::pending::<()>();
|
|
||||||
|
|
||||||
tokio::select! {
|
|
||||||
_ = ctrl_c => {},
|
|
||||||
_ = terminate => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Signal received, shutting down");
|
|
||||||
}
|
|
@@ -1,3 +0,0 @@
|
|||||||
mod article;
|
|
||||||
mod summary;
|
|
||||||
mod user;
|
|
@@ -1,2 +0,0 @@
|
|||||||
mod summary_service;
|
|
||||||
mod news_service;
|
|
@@ -8,11 +8,11 @@ MIN_CRON_HOURS = float(os.getenv("MIN_CRON_HOURS", 0.5))
|
|||||||
DEFAULT_CRON_HOURS = float(os.getenv("CRON_HOURS", MIN_CRON_HOURS))
|
DEFAULT_CRON_HOURS = float(os.getenv("CRON_HOURS", MIN_CRON_HOURS))
|
||||||
CRON_HOURS = max(MIN_CRON_HOURS, DEFAULT_CRON_HOURS)
|
CRON_HOURS = max(MIN_CRON_HOURS, DEFAULT_CRON_HOURS)
|
||||||
SYNC_COOLDOWN_MINUTES = int(os.getenv("SYNC_COOLDOWN_MINUTES", 30))
|
SYNC_COOLDOWN_MINUTES = int(os.getenv("SYNC_COOLDOWN_MINUTES", 30))
|
||||||
LLM_MODEL = os.getenv("LLM_MODEL", "mistral-nemo:12b")
|
LLM_MODEL = os.getenv("LLM_MODEL", "gemma2:9b")
|
||||||
LLM_TIMEOUT_SECONDS = int(os.getenv("LLM_TIMEOUT_SECONDS", 180))
|
LLM_TIMEOUT_SECONDS = int(os.getenv("LLM_TIMEOUT_SECONDS", 180))
|
||||||
OLLAMA_API_TIMEOUT_SECONDS = int(os.getenv("OLLAMA_API_TIMEOUT_SECONDS", 10))
|
OLLAMA_API_TIMEOUT_SECONDS = int(os.getenv("OLLAMA_API_TIMEOUT_SECONDS", 10))
|
||||||
ARTICLE_FETCH_TIMEOUT = int(os.getenv("ARTICLE_FETCH_TIMEOUT", 30))
|
ARTICLE_FETCH_TIMEOUT = int(os.getenv("ARTICLE_FETCH_TIMEOUT", 30))
|
||||||
MAX_ARTICLE_LENGTH = int(os.getenv("MAX_ARTICLE_LENGTH", 5000))
|
MAX_ARTICLE_LENGTH = int(os.getenv("MAX_ARTICLE_LENGTH", 40_000))
|
||||||
|
|
||||||
frontend_path = os.path.join(
|
frontend_path = os.path.join(
|
||||||
os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
|
os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
|
||||||
@@ -21,7 +21,7 @@ frontend_path = os.path.join(
|
|||||||
)
|
)
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.WARNING,
|
level=logging.DEBUG,
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@@ -150,8 +150,6 @@ async def get_news(
|
|||||||
where_conditions.append("published BETWEEN ? AND ?")
|
where_conditions.append("published BETWEEN ? AND ?")
|
||||||
params.extend([from_ts, to_ts])
|
params.extend([from_ts, to_ts])
|
||||||
|
|
||||||
logger.info(f"Date range: {from_date} to {to_date} (UTC timestamps: {from_ts} to {to_ts})")
|
|
||||||
|
|
||||||
# Build the complete SQL query
|
# Build the complete SQL query
|
||||||
base_sql = """
|
base_sql = """
|
||||||
SELECT id, title, summary, url, published, country, created_at
|
SELECT id, title, summary, url, published, country, created_at
|
||||||
@@ -163,27 +161,13 @@ async def get_news(
|
|||||||
else:
|
else:
|
||||||
sql = base_sql
|
sql = base_sql
|
||||||
|
|
||||||
sql += " ORDER BY published DESC LIMIT 1000"
|
sql += " ORDER BY published DESC"
|
||||||
|
|
||||||
# Log query info
|
|
||||||
if all_countries and all_dates:
|
|
||||||
logger.info("Querying ALL news articles (no filters)")
|
|
||||||
elif all_countries:
|
|
||||||
logger.info(f"Querying news from ALL countries with date filter")
|
|
||||||
elif all_dates:
|
|
||||||
logger.info(f"Querying ALL dates for countries: {country}")
|
|
||||||
else:
|
|
||||||
logger.info(f"Querying news: countries={country}, timezone={timezone_name}")
|
|
||||||
|
|
||||||
logger.info(f"SQL: {sql}")
|
|
||||||
logger.info(f"Parameters: {params}")
|
|
||||||
|
|
||||||
# Execute the query
|
# Execute the query
|
||||||
db.execute(sql, params)
|
db.execute(sql, params)
|
||||||
rows = db.fetchall()
|
rows = db.fetchall()
|
||||||
result = [dict(row) for row in rows]
|
result = [dict(row) for row in rows]
|
||||||
|
|
||||||
logger.info(f"Found {len(result)} news articles")
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
@@ -124,7 +124,6 @@ class NewsFetcher:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def build_prompt(
|
def build_prompt(
|
||||||
url: str,
|
|
||||||
title: str = "",
|
title: str = "",
|
||||||
summary: str = "",
|
summary: str = "",
|
||||||
content: str = "") -> str:
|
content: str = "") -> str:
|
||||||
@@ -132,14 +131,13 @@ class NewsFetcher:
|
|||||||
Generate a prompt for the LLM to summarize an article.
|
Generate a prompt for the LLM to summarize an article.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
url: Public URL of the article to summarize
|
|
||||||
title: Article title from RSS feed (optional)
|
title: Article title from RSS feed (optional)
|
||||||
summary: Article summary from RSS feed (optional)
|
summary: Article summary from RSS feed (optional)
|
||||||
content: Extracted article content (optional)
|
content: Extracted article content (optional)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A formatted prompt string that instructs the LLM to generate
|
A formatted prompt string that instructs the LLM to generate
|
||||||
a JSON response with title and summaries in German and English
|
a JSON response with title, summary and tags in German
|
||||||
"""
|
"""
|
||||||
context_info = []
|
context_info = []
|
||||||
if title:
|
if title:
|
||||||
@@ -155,21 +153,36 @@ class NewsFetcher:
|
|||||||
context_info) if context_info else "Keine zusätzlichen Informationen verfügbar."
|
context_info) if context_info else "Keine zusätzlichen Informationen verfügbar."
|
||||||
|
|
||||||
return (
|
return (
|
||||||
"### Aufgabe\n"
|
"### Vorliegende Informationen\n"
|
||||||
f"Du sollst eine Nachricht basierend auf der URL und den verfügbaren Informationen zusammenfassen.\n"
|
f"{context}\n\n"
|
||||||
f"URL: {url}\n"
|
"### Längenbegrenzungen\n"
|
||||||
f"Verfügbare Informationen:\n{context}\n\n"
|
"title: Format \"ORT: Titel\", max 100 Zeichen\n"
|
||||||
|
"location: nur der ORT-Teil, max 40 Zeichen\n"
|
||||||
|
"summary: 100–160 Wörter\n"
|
||||||
|
"tags: bis zu 6 Schlüsselwörter, durch Komma getrennt, alles Kleinbuchstaben.\n\n"
|
||||||
"### Regeln\n"
|
"### Regeln\n"
|
||||||
"1. Nutze VORRANGIG den Artikel-Inhalt falls verfügbar, ergänze mit RSS-Informationen\n"
|
"1. Nutze ausschließlich Informationen, die im bereitgestellten Material eindeutig vorkommen. Externes Wissen ist untersagt.\n"
|
||||||
"2. Falls kein Artikel-Inhalt verfügbar ist, nutze RSS-Titel und -Beschreibung\n"
|
"2. Liegt sowohl Artikel-Text als auch RSS-Metadaten vor, hat der Artikel-Text Vorrang; verwende RSS nur ergänzend.\n"
|
||||||
"3. Falls keine ausreichenden Informationen vorliegen, erstelle eine plausible Zusammenfassung basierend auf der URL\n"
|
"3. Liegt nur RSS-Titel und/oder -Beschreibung vor, stütze dich ausschließlich darauf.\n"
|
||||||
"4. Gib ausschließlich **gültiges minifiziertes JSON** zurück – kein Markdown, keine Kommentare\n"
|
"4. Sind die Informationen unzureichend, gib exakt {\"location\":\"\",\"title\":\"\",\"summary\":\"\",\"tags\":\"\"} zurück.\n"
|
||||||
"5. Struktur: {\"title\":\"…\",\"summary\":\"…\"}\n"
|
"5. Gib nur gültiges, minifiziertes JSON zurück – keine Zeilenumbrüche, kein Markdown, keine Kommentare.\n"
|
||||||
"6. title: Aussagekräftiger deutscher Titel (max 100 Zeichen)\n"
|
"6. Verwende keine hypothetischen Formulierungen (\"könnte\", \"möglicherweise\" etc.).\n"
|
||||||
"7. summary: Deutsche Zusammenfassung (zwischen 100 und 160 Wörter)\n"
|
"7. Wörtliche Zitate dürfen höchstens 15 % des Summary-Texts ausmachen.\n"
|
||||||
"8. Kein Text vor oder nach dem JSON\n\n"
|
"8. Kein Text vor oder nach dem JSON.\n\n"
|
||||||
"### Ausgabe\n"
|
"### Ausgabe\n"
|
||||||
"Jetzt antworte mit dem JSON:"
|
"Antworte jetzt ausschließlich mit dem JSON:\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def build_system_prompt():
|
||||||
|
return (
|
||||||
|
"Du bist ein hochpräziser JSON-Summarizer und Experte für die Zusammenfassung von Artikeln.\n\n"
|
||||||
|
"### Vorgehen\n"
|
||||||
|
"Schritt 1: Identifiziere Hauptthema und Zweck.\n"
|
||||||
|
"Schritt 2: Extrahiere die wichtigsten Fakten und Ergebnisse.\n"
|
||||||
|
"Schritt 3: Erkenne die zentralen Argumente und Standpunkte.\n"
|
||||||
|
"Schritt 4: Ordne die Informationen nach Wichtigkeit.\n"
|
||||||
|
"Schritt 5: Erstelle eine prägnante, klare und sachliche Zusammenfassung.\n\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -193,26 +206,55 @@ class NewsFetcher:
|
|||||||
A dictionary containing the article title and summaries in German and English,
|
A dictionary containing the article title and summaries in German and English,
|
||||||
or None if summarization failed
|
or None if summarization failed
|
||||||
"""
|
"""
|
||||||
|
logger.debug("[AI] Fetching article content from: " + url)
|
||||||
|
|
||||||
article_content = await NewsFetcher.fetch_article_content(client, url)
|
article_content = await NewsFetcher.fetch_article_content(client, url)
|
||||||
|
|
||||||
if not article_content:
|
if not article_content:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"⚠️ Could not fetch article content, using RSS data only")
|
f"⚠️ Could not fetch article content, using RSS data only")
|
||||||
|
|
||||||
prompt = NewsFetcher.build_prompt(
|
prompt = NewsFetcher.build_prompt(title, summary, article_content)
|
||||||
url, title, summary, article_content)
|
system_prompt = NewsFetcher.build_system_prompt()
|
||||||
payload = {
|
payload = {
|
||||||
"model": LLM_MODEL,
|
"model": LLM_MODEL,
|
||||||
"prompt": prompt,
|
"prompt": prompt,
|
||||||
|
"system": system_prompt,
|
||||||
"stream": False,
|
"stream": False,
|
||||||
"temperature": 0.1,
|
"temperature": 0.1,
|
||||||
"format": "json",
|
"format": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"title",
|
||||||
|
"summary",
|
||||||
|
"tags"
|
||||||
|
]
|
||||||
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"num_gpu": 1, # Force GPU usage
|
"num_gpu": 1, # Force GPU usage
|
||||||
"num_ctx": 8192, # Context size
|
"num_ctx": 8192, # Context size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug("[AI] Running summary generation...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
f"{OLLAMA_HOST}/api/generate",
|
f"{OLLAMA_HOST}/api/generate",
|
||||||
@@ -224,6 +266,8 @@ class NewsFetcher:
|
|||||||
result = response.json()
|
result = response.json()
|
||||||
llm_response = result["response"]
|
llm_response = result["response"]
|
||||||
|
|
||||||
|
logger.debug("[AI] " + llm_response)
|
||||||
|
|
||||||
if isinstance(llm_response, str):
|
if isinstance(llm_response, str):
|
||||||
summary_data = json.loads(llm_response)
|
summary_data = json.loads(llm_response)
|
||||||
else:
|
else:
|
||||||
@@ -388,8 +432,6 @@ class NewsFetcher:
|
|||||||
summary=rss_summary
|
summary=rss_summary
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(summary)
|
|
||||||
|
|
||||||
if not summary:
|
if not summary:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"❌ Failed to get summary for article {i}: {article_url}")
|
f"❌ Failed to get summary for article {i}: {article_url}")
|
||||||
@@ -403,7 +445,8 @@ class NewsFetcher:
|
|||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
INSERT
|
INSERT
|
||||||
OR IGNORE INTO news
|
OR IGNORE
|
||||||
|
INTO news
|
||||||
(title, summary, url, published, country)
|
(title, summary, url, published, country)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
|
@@ -11,9 +11,12 @@ MIN_CRON_HOURS=0.5
|
|||||||
SYNC_COOLDOWN_MINUTES=30
|
SYNC_COOLDOWN_MINUTES=30
|
||||||
|
|
||||||
# LLM model to use for summarization
|
# LLM model to use for summarization
|
||||||
LLM_MODEL=qwen2:7b-instruct-q4_K_M
|
LLM_MODEL=qwen2:7b-instruct-q4_K_M # ca 7-9GB (typisch 8GB)
|
||||||
LLM_MODEL=phi3:3.8b-mini-128k-instruct-q4_0
|
LLM_MODEL=phi3:3.8b-mini-128k-instruct-q4_0 # ca 6-8GB (langer kontext)
|
||||||
LLM_MODEL=mistral-nemo:12b
|
LLM_MODEL=mistral-nemo:12b # ca 16-24+GB
|
||||||
|
LLM_MODEL=cnjack/mistral-samll-3.1:24b-it-q4_K_S # ca 22GB
|
||||||
|
LLM_MODEL=yarn-mistral:7b-64k-q4_K_M # ca 11GB
|
||||||
|
LLM_MODEL=gemma2:9b # ca 8GB
|
||||||
|
|
||||||
# Timeout in seconds for LLM requests
|
# Timeout in seconds for LLM requests
|
||||||
LLM_TIMEOUT_SECONDS=180
|
LLM_TIMEOUT_SECONDS=180
|
||||||
|
Binary file not shown.
@@ -1,8 +0,0 @@
|
|||||||
import { defineConfig } from 'cypress'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
e2e: {
|
|
||||||
specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}',
|
|
||||||
baseUrl: 'http://localhost:4173',
|
|
||||||
},
|
|
||||||
})
|
|
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "owly-news-summariser",
|
"name": "owly-news",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "PolyForm-Noncommercial-1.0.0",
|
"license": "PolyForm-Noncommercial-1.0.0",
|
||||||
|
@@ -14,9 +14,10 @@
|
|||||||
|
|
||||||
<!-- Articles Grid -->
|
<!-- Articles Grid -->
|
||||||
<div v-else class="grid gap-4 sm:gap-6 md:grid-cols-2 xl:grid-cols-3">
|
<div v-else class="grid gap-4 sm:gap-6 md:grid-cols-2 xl:grid-cols-3">
|
||||||
|
<template v-for="article in news.articles"
|
||||||
|
:key="article.id">
|
||||||
<article
|
<article
|
||||||
v-for="article in news.articles"
|
v-if="isValidArticleContent(article)"
|
||||||
:key="article.id"
|
|
||||||
class="flex flex-col h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 hover:shadow-md dark:hover:shadow-lg dark:hover:shadow-gray-800/50 transition-all duration-200 overflow-hidden group"
|
class="flex flex-col h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 hover:shadow-md dark:hover:shadow-lg dark:hover:shadow-gray-800/50 transition-all duration-200 overflow-hidden group"
|
||||||
>
|
>
|
||||||
<!-- Article Header -->
|
<!-- Article Header -->
|
||||||
@@ -28,7 +29,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<time
|
<time
|
||||||
:datetime="new Date(article.published * 1000).toISOString()"
|
:datetime="new Date(article.published * 1000).toISOString()"
|
||||||
:title="new Date(article.published * 1000).toLocaleString(userLocale.value, {
|
:title="new Date(article.published * 1000).toLocaleString(userLocale, {
|
||||||
dateStyle: 'full',
|
dateStyle: 'full',
|
||||||
timeStyle: 'long'
|
timeStyle: 'long'
|
||||||
})"
|
})"
|
||||||
@@ -83,6 +84,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading State & Load More Trigger -->
|
<!-- Loading State & Load More Trigger -->
|
||||||
@@ -129,6 +131,37 @@ const loadMoreArticles = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface Article {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
summary: string;
|
||||||
|
url: string;
|
||||||
|
published: number;
|
||||||
|
country: string;
|
||||||
|
created_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const INVALID_MARKERS = ['---', '...', '…', 'Title', 'Summary', 'Titel', 'Zusammenfassung'] as const;
|
||||||
|
const REQUIRED_TEXT_FIELDS = ['title', 'summary', 'url'] as const;
|
||||||
|
|
||||||
|
const isValidArticleContent = (article: Article): boolean => {
|
||||||
|
const hasEmptyRequiredFields = REQUIRED_TEXT_FIELDS.some(
|
||||||
|
field => article[field as keyof Pick<Article, typeof REQUIRED_TEXT_FIELDS[number]>].length === 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasEmptyRequiredFields) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasInvalidMarkers = REQUIRED_TEXT_FIELDS.some(field =>
|
||||||
|
INVALID_MARKERS.some(marker =>
|
||||||
|
article[field as keyof Pick<Article, typeof REQUIRED_TEXT_FIELDS[number]>].includes(marker)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return !hasInvalidMarkers;
|
||||||
|
};
|
||||||
|
|
||||||
const observer = ref<IntersectionObserver | null>(null);
|
const observer = ref<IntersectionObserver | null>(null);
|
||||||
const loadMoreTrigger = ref<HTMLElement | null>(null);
|
const loadMoreTrigger = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
@@ -6470,9 +6470,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"owly-news-summariser@workspace:.":
|
"owly-news@workspace:.":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "owly-news-summariser@workspace:."
|
resolution: "owly-news@workspace:."
|
||||||
dependencies:
|
dependencies:
|
||||||
"@tailwindcss/vite": "npm:^4.1.11"
|
"@tailwindcss/vite": "npm:^4.1.11"
|
||||||
"@tsconfig/node22": "npm:^22.0.2"
|
"@tsconfig/node22": "npm:^22.0.2"
|
||||||
|
Reference in New Issue
Block a user