Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates frontend #9

Merged
merged 14 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,8 @@ frontend/**/*.sw?
# flyctl launch added from node_modules/tailwindcss/stubs/.gitignore
!node_modules/tailwindcss/stubs/**/*
fly.toml

# workspace
landing-page
docs
.github
18 changes: 18 additions & 0 deletions .github/workflows/fly-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/

name: Fly Deploy
on:
push:
branches:
- main
jobs:
deploy:
name: Deploy app
runs-on: ubuntu-latest
concurrency: deploy-group # optional: ensure only one action runs at a time
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
77 changes: 49 additions & 28 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,52 +1,73 @@
FROM oven/bun as deps
## NOTE
# This Dockerfile builds the frontend and backend separately,
# frontend uses npm and backend requires bun.
# This separation is a temporary solution for a Bun issue with rsbuild,
# see: https://github.com/oven-sh/bun/issues/11628

# Frontend deps & build stage
FROM node:20 as frontend-builder
WORKDIR /app

# Copy package files for all workspaces
COPY package.json bun.lockb turbo.json ./
# Copy frontend package files
COPY frontend/package.json ./frontend/
COPY backend/package.json ./backend/

# Install dependencies
RUN bun install
# Install frontend dependencies
RUN cd frontend && npm install

# Copy frontend source code
COPY frontend ./frontend

# Build frontend
RUN cd frontend && npm run build

# Build stage
FROM oven/bun as builder
# Backend deps & build stage
FROM oven/bun as backend-builder
WORKDIR /app

# Set NODE_ENV for build process
ENV NODE_ENV="production"
# Copy backend package files
COPY package.json ./
COPY backend/package.json ./backend/

# Copy all files from deps stage including node_modules
COPY --from=deps /app ./
# Install backend dependencies
RUN cd backend && bun install

# Copy source code
COPY . .
# Copy backend source code
COPY backend ./backend

# Build both frontend and backend
RUN bun run build
# Build backend
RUN cd backend && bun run build

# Production stage
FROM oven/bun as production
WORKDIR /app

# Create directory for mount with correct permissions
RUN mkdir -p /.data/db /.data/cache && \
chown -R bun:bun /.data
# Install LiteFS dependencies
RUN apt-get update -y && apt-get install -y ca-certificates fuse3 sqlite3

# Copy only necessary files from builder
COPY --from=builder --chown=bun:bun /app/package.json /app/bun.lockb /app/turbo.json ./
COPY --from=builder --chown=bun:bun /app/node_modules ./node_modules
COPY --from=builder --chown=bun:bun /app/frontend/dist ./frontend/dist
COPY --from=builder --chown=bun:bun /app/backend/dist ./backend/dist
# Copy LiteFS binary
COPY --from=flyio/litefs:0.5 /usr/local/bin/litefs /usr/local/bin/litefs

# Create directories for mounts with correct permissions
RUN mkdir -p /litefs /var/lib/litefs && \
chown -R bun:bun /litefs /var/lib/litefs

# Create volume mount points
# Copy only necessary files from builders
COPY --from=backend-builder --chown=bun:bun /app/package.json ./
COPY --chown=bun:bun curate.config.json ./

COPY --from=frontend-builder --chown=bun:bun /app/frontend/dist ./frontend/dist
COPY --from=backend-builder --chown=bun:bun /app/backend/dist ./backend/dist

# Set environment variables
ENV DATABASE_URL="file:/.data/db/sqlite.db"
ENV CACHE_DIR="/.data/cache"
ENV DATABASE_URL="file:/litefs/db"
ENV NODE_ENV="production"

# Expose the port
EXPOSE 3000

# Start the application using the production start script
CMD ["bun", "run", "start"]
# Copy LiteFS configuration
COPY --chown=bun:bun litefs.yml /etc/litefs.yml

# Start LiteFS (runs app with distributed file system for SQLite)
ENTRYPOINT ["litefs", "mount"]
13 changes: 4 additions & 9 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,13 @@
TWITTER_USERNAME=your_twitter_username
TWITTER_PASSWORD=your_twitter_password
TWITTER_EMAIL=your_twitter_email
TWITTER_2FA_SECRET=your_twitter_2fa

# Environment
NODE_ENV=development

# Telegram Export Configuration
TELEGRAM_ENABLED=false
#PLUGINS

# Telegram Distributor Configuration
TELEGRAM_BOT_TOKEN=your_bot_token
TELEGRAM_CHANNEL_ID=your_channel_id

# RSS Export Configuration
RSS_ENABLED=false
RSS_TITLE=Public Goods News
RSS_DESCRIPTION=Latest approved public goods submissions
RSS_FEED_PATH=public/feed.xml
RSS_MAX_ITEMS=100
5 changes: 1 addition & 4 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,8 @@
]
},
"devDependencies": {
"@types/express": "^4.17.17",
"@types/jest": "^29.5.11",
"@types/node": "^20.10.6",
"@types/ora": "^3.2.0",
"bun-types": "^1.1.40",
"bun-types": "^1.1.43",
"drizzle-kit": "^0.30.1",
"jest": "^29.7.0",
"jest-mock-extended": "^4.0.0-beta1",
Expand Down
110 changes: 0 additions & 110 deletions backend/src/external/rss.ts

This file was deleted.

113 changes: 113 additions & 0 deletions backend/src/external/rss/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { writeFile, mkdir } from "fs/promises";
import path from "path";
import { DistributorPlugin } from "types/plugin";
import { RssService } from "./rss.service";
import type { RssItem } from "../../services/rss/queries";
import type { DBOperations } from "../../services/db/operations";

export default class RssPlugin implements DistributorPlugin {
name = "@curatedotfun/rss";
private services: Map<string, RssService> = new Map();
private dbOps?: DBOperations;

getServices(): Map<string, RssService> {
return this.services;
}

constructor(dbOperations?: DBOperations) {
this.dbOps = dbOperations;
}

async initialize(
feedId: string,
config: Record<string, string>,
): Promise<void> {
if (!config.title) {
throw new Error("RSS plugin requires title");
}

const maxItems = config.maxItems ? parseInt(config.maxItems) : 100;

// Create a new RSS service for this feed
const service = new RssService(
feedId,
config.title,
maxItems,
config.path,
this.dbOps,
);

this.services.set(feedId, service);
}

async distribute(feedId: string, content: string): Promise<void> {
const service = this.services.get(feedId);
if (!service) {
throw new Error("RSS plugin not initialized for this feed");
}

const item: RssItem = {
title: "New Update",
content,
link: "https://twitter.com/", // TODO: Update with actual link
publishedAt: new Date().toISOString(),
guid: Date.now().toString(),
};

// Save to database
service.saveItem(item);

// Write to file if path is provided (backward compatibility)
const path = service.getPath();
if (path) {
await this.writeToFile(service, path);
}
}

private async writeToFile(
service: RssService,
filePath: string,
): Promise<void> {
const items = service.getItems();

const feed = `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>${service.getTitle()}</title>
<link>https://twitter.com/</link>
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
${items
.map(
(item) => `
<item>
<title>${this.escapeXml(item.title || "")}</title>
<description>${this.escapeXml(item.content)}</description>
<link>${item.link || ""}</link>
<pubDate>${new Date(item.publishedAt).toUTCString()}</pubDate>
<guid>${item.guid || ""}</guid>
</item>`,
)
.join("\n")}
</channel>
</rss>`;

// Ensure directory exists
const dir = path.dirname(filePath);
await mkdir(dir, { recursive: true });
await writeFile(filePath, feed, "utf-8");
}

private escapeXml(unsafe: string): string {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}

async shutdown(): Promise<void> {
// Clear all services when plugin shuts down
this.services.clear();
}
}
Loading
Loading