Skip to content

Commit 6b0a1ae

Browse files
committed
Added Docker
1 parent 926dfff commit 6b0a1ae

File tree

8 files changed

+172
-58
lines changed

8 files changed

+172
-58
lines changed

.dockerignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.git
2+
node_modules
3+
npm-debug.log
4+
Dockerfile
5+
.dockerignore
6+
.env
7+
*.md
8+
.eslintrc*
9+
dist
10+
coverage
11+
.vscode
12+
data/
13+
history.json

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,5 @@ dist-ssr
2525

2626
docs/
2727

28-
config.json
29-
history.json
28+
# Data directory
29+
data/

Dockerfile

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Build stage
2+
FROM node:20-slim AS builder
3+
4+
WORKDIR /app
5+
6+
# Copy package files
7+
COPY package*.json ./
8+
9+
# Install dependencies
10+
RUN npm ci
11+
12+
# Copy source code
13+
COPY . .
14+
15+
# Build the application
16+
RUN npm run build
17+
18+
# Production stage
19+
FROM node:20-slim AS production
20+
21+
WORKDIR /app
22+
23+
# Copy package files and install production dependencies
24+
COPY package*.json ./
25+
RUN npm ci --production
26+
27+
# Copy built files from builder
28+
COPY --from=builder /app/dist ./dist
29+
COPY --from=builder /app/src ./src
30+
31+
# Create data directory and ensure it exists
32+
RUN mkdir -p /app/data
33+
34+
# Expose the port the app runs on
35+
EXPOSE 3000
36+
37+
# Set environment variables
38+
ENV NODE_ENV=production
39+
ENV RSS_WATCHER_DATA_DIR=/app/data
40+
41+
# Start the application
42+
CMD ["npm", "start"]

README.md

Lines changed: 94 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,104 @@
1-
# React + TypeScript + Vite
1+
# RSS Watcher
22

3-
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
3+
A modern web application for monitoring RSS feeds and receiving notifications when new posts match your keywords. Built with React, Express, and TypeScript.
44

5-
Currently, two official plugins are available:
5+
## Features
66

7-
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8-
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
7+
- 🔍 Monitor multiple RSS feeds simultaneously
8+
- 🔑 Define custom keywords for each feed
9+
- 🔔 Real-time notifications via [ntfy.sh](https://ntfy.sh)
10+
- ⏰ Configurable check intervals
11+
- 📱 Responsive web interface
12+
- 📜 Detailed feed history tracking
13+
- 🚦 Live feed status monitoring
914

10-
## Expanding the ESLint configuration
15+
## Getting Started
1116

12-
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
17+
### Prerequisites
1318

14-
- Configure the top-level `parserOptions` property like this:
19+
- Node.js (v14 or higher)
20+
- npm or yarn
1521

16-
```js
17-
export default tseslint.config({
18-
languageOptions: {
19-
// other options...
20-
parserOptions: {
21-
project: ['./tsconfig.node.json', './tsconfig.app.json'],
22-
tsconfigRootDir: import.meta.dirname,
23-
},
24-
},
25-
})
22+
### Installation
23+
24+
1. Clone the repository:
25+
```bash
26+
git clone https://github.com/ablomer/rsswatcher.git
27+
cd rsswatcher
28+
```
29+
30+
2. Install dependencies:
31+
```bash
32+
npm install
33+
# or
34+
yarn install
35+
```
36+
37+
### Development
38+
39+
Run the development server:
40+
41+
```bash
42+
npm run dev
43+
# or
44+
yarn dev
2645
```
2746

28-
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
29-
- Optionally add `...tseslint.configs.stylisticTypeChecked`
30-
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
31-
32-
```js
33-
// eslint.config.js
34-
import react from 'eslint-plugin-react'
35-
36-
export default tseslint.config({
37-
// Set the react version
38-
settings: { react: { version: '18.3' } },
39-
plugins: {
40-
// Add the react plugin
41-
react,
42-
},
43-
rules: {
44-
// other rules...
45-
// Enable its recommended rules
46-
...react.configs.recommended.rules,
47-
...react.configs['jsx-runtime'].rules,
48-
},
49-
})
47+
### Production
48+
49+
Build and start the production server:
50+
51+
```bash
52+
npm run build
53+
npm start
54+
# or
55+
yarn build
56+
yarn start
5057
```
58+
59+
Once the server is running, visit `http://localhost:3000` in your browser to access the web interface.
60+
61+
## Configuration
62+
63+
All configuration is managed through the web interface:
64+
65+
### Feed Management
66+
- Add, edit, or remove RSS feeds through the "Feeds" tab
67+
- Set keywords for each feed to monitor
68+
- View feed status and history
69+
70+
### Settings
71+
- Configure ntfy.sh notification settings in the "Settings" tab
72+
- Set your ntfy topic
73+
- Configure ntfy server address (defaults to https://ntfy.sh)
74+
- Adjust feed check interval
75+
76+
## Docker Support
77+
78+
Build the Docker image:
79+
80+
```bash
81+
docker build -t rsswatcher .
82+
```
83+
84+
Run the container:
85+
86+
```bash
87+
# Create a directory for persistent data
88+
mkdir -p data
89+
90+
# Run the container with mounted data directory
91+
docker run -p 3000:3000 \
92+
-v $(pwd)/data:/app/data \
93+
rsswatcher
94+
```
95+
96+
This will persist all application data (configuration and history) across container restarts.
97+
98+
## Contributing
99+
100+
Contributions are welcome! Please feel free to submit a Pull Request.
101+
102+
## License
103+
104+
This project is licensed under the MIT License (see the [LICENSE](LICENSE) file for details).

src/server/config.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import fs from 'fs';
22
import path from 'path';
33
import { AppConfig } from './types';
4+
import { DEFAULT_DATA_DIR } from './constants';
45

5-
const CONFIG_FILE = path.join(process.cwd(), 'config.json');
6+
const DATA_DIR = process.env.RSS_WATCHER_DATA_DIR || DEFAULT_DATA_DIR;
7+
const CONFIG_FILE = path.join(DATA_DIR, 'config.json');
68

79
const DEFAULT_CONFIG: AppConfig = {
810
feeds: [],
@@ -15,6 +17,10 @@ export class ConfigManager {
1517
private config: AppConfig;
1618

1719
constructor() {
20+
// Ensure data directory exists
21+
if (!fs.existsSync(DATA_DIR)) {
22+
fs.mkdirSync(DATA_DIR, { recursive: true });
23+
}
1824
this.config = this.loadConfig();
1925
}
2026

src/server/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const DEFAULT_DATA_DIR = '/app/data';

src/server/feedMonitor.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export class FeedMonitor {
7878
});
7979

8080
const matchingItems = newItems.map((item: RssItem) => ({
81-
title: item.title || '',
81+
title: (item.title || '').trim(),
8282
link: item.link || '',
8383
description: item['dc:content'] || item['content:encoded'] || item.content || item.summary || item.contentSnippet || '',
8484
content: item['dc:content'] || item['content:encoded'] || item.content || '',
@@ -119,7 +119,7 @@ export class FeedMonitor {
119119
this.postHistoryManager.addCheckedPost(
120120
item.guid,
121121
url,
122-
item.title || '',
122+
(item.title || '').trim(),
123123
item.link || '',
124124
matchedKeywords,
125125
matchedKeywords.length > 0 // notification sent if keywords matched
@@ -151,9 +151,9 @@ export class FeedMonitor {
151151
private async sendNotification(item: FeedItem) {
152152
const config = this.configManager.getConfig();
153153
const ntfyUrl = `${config.ntfyServerAddress}/${config.ntfyTopic}`;
154+
// console.log(`Sending notification for ${item.title}`);
154155

155156
try {
156-
console.log(`Sending notification for ${item.title}`);
157157
// Sanitize the title by removing any characters
158158
// that aren't in the ASCII printable range (0x20 to 0x7E)
159159
const sanitizedTitle = item.title.replace(/[^\x20-\x7E]/g, '');

src/server/postHistory.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
import fs from 'fs';
22
import path from 'path';
33
import { PostHistory, FeedHistoryEntry } from './types';
4+
import { DEFAULT_DATA_DIR } from './constants';
45

5-
const POST_HISTORY_FILE = 'history.json';
6+
const DATA_DIR = process.env.RSS_WATCHER_DATA_DIR || DEFAULT_DATA_DIR;
7+
const POST_HISTORY_FILE = path.join(DATA_DIR, 'history.json');
68

79
export class PostHistoryManager {
810
private history: PostHistory = {};
9-
private filePath: string;
1011

1112
constructor() {
12-
this.filePath = path.join(process.cwd(), POST_HISTORY_FILE);
13+
// Ensure data directory exists
14+
if (!fs.existsSync(DATA_DIR)) {
15+
fs.mkdirSync(DATA_DIR, { recursive: true });
16+
}
1317
this.loadHistory();
1418
}
1519

1620
private loadHistory() {
1721
try {
18-
if (fs.existsSync(this.filePath)) {
19-
const data = fs.readFileSync(this.filePath, 'utf-8');
22+
if (fs.existsSync(POST_HISTORY_FILE)) {
23+
const data = fs.readFileSync(POST_HISTORY_FILE, 'utf-8');
2024
this.history = JSON.parse(data);
2125
}
2226
} catch (error) {
@@ -27,13 +31,7 @@ export class PostHistoryManager {
2731

2832
private saveHistory() {
2933
try {
30-
// Ensure the directory exists
31-
const dir = path.dirname(this.filePath);
32-
if (!fs.existsSync(dir)) {
33-
fs.mkdirSync(dir, { recursive: true });
34-
}
35-
36-
fs.writeFileSync(this.filePath, JSON.stringify(this.history, null, 2));
34+
fs.writeFileSync(POST_HISTORY_FILE, JSON.stringify(this.history, null, 2));
3735
} catch (error) {
3836
console.error('Error saving post history:', error);
3937
}

0 commit comments

Comments
 (0)