Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63f0bdbab6 | |||
| 4648614556 | |||
| 53f01acb0c |
@@ -52,12 +52,17 @@ jobs:
|
||||
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
SHA=$(git rev-parse --short HEAD)
|
||||
if [ "${{ gitea.ref_type }}" = "tag" ]; then
|
||||
NEWTAG="${{ gitea.ref_name }}"
|
||||
else
|
||||
NEWTAG="sha-${SHA}"
|
||||
fi
|
||||
git clone "https://admin:${GITEA_TOKEN}@git.kubebase.de/admin/apps.git" /tmp/apps
|
||||
cd /tmp/apps
|
||||
cd apps/system/toolrate/overlays/k3s
|
||||
sed -i "s|newTag: .*|newTag: sha-${SHA}|" kustomization.yaml
|
||||
sed -i "s|newTag: .*|newTag: ${NEWTAG}|" kustomization.yaml
|
||||
git config user.name "Gitea Actions"
|
||||
git config user.email "actions@git.kubebase.de"
|
||||
git add -A
|
||||
git diff --cached --quiet || git commit -m "chore: update toolrate image to sha-${SHA}"
|
||||
git diff --cached --quiet || git commit -m "chore: update toolrate image to ${NEWTAG}"
|
||||
git push
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
modules = ["nodejs-24", "postgresql-16"]
|
||||
|
||||
[deployment]
|
||||
router = "application"
|
||||
deploymentTarget = "autoscale"
|
||||
|
||||
[deployment.postBuild]
|
||||
args = ["pnpm", "store", "prune"]
|
||||
env = { "CI" = "true" }
|
||||
|
||||
[workflows]
|
||||
runButton = "Project"
|
||||
|
||||
[agent]
|
||||
stack = "PNPM_WORKSPACE"
|
||||
expertMode = true
|
||||
|
||||
[postMerge]
|
||||
path = "scripts/post-merge.sh"
|
||||
timeoutMs = 20000
|
||||
|
||||
[[ports]]
|
||||
localPort = 8080
|
||||
externalPort = 80
|
||||
|
||||
[[ports]]
|
||||
localPort = 8081
|
||||
externalPort = 8081
|
||||
|
||||
[[ports]]
|
||||
localPort = 26015
|
||||
externalPort = 3000
|
||||
|
||||
[nix]
|
||||
channel = "stable-25_05"
|
||||
@@ -1,5 +0,0 @@
|
||||
# The format of this file is identical to `.dockerignore`.
|
||||
# It is used to reduce the size of deployed images to make the process of publishing faster.
|
||||
|
||||
# No need to store the pnpm store twice.
|
||||
.local
|
||||
+38
-35
@@ -2,10 +2,30 @@
|
||||
|
||||
Community platform for listing and rating developer/productivity tools by usefulness and usability.
|
||||
|
||||
## Run & Operate
|
||||
## Stack
|
||||
|
||||
- `pnpm --filter @workspace/api-server run dev` — run the API server (port 8080)
|
||||
- `pnpm --filter @workspace/toolrate run dev` — run the frontend (port 26015)
|
||||
- pnpm workspaces, Node.js 24, TypeScript 5.9
|
||||
- API: Express 5 + express-session + connect-pg-simple
|
||||
- DB: PostgreSQL + Drizzle ORM
|
||||
- Validation: Zod (`zod/v4`), `drizzle-zod`
|
||||
- API codegen: Orval (from OpenAPI spec)
|
||||
- Build: esbuild (CJS bundle)
|
||||
- Frontend: React 19 + Vite + TanStack Query + wouter + shadcn/ui + recharts
|
||||
|
||||
## Packages
|
||||
|
||||
- `lib/api-spec` — OpenAPI contract (source of truth)
|
||||
- `lib/api-zod` — generated Zod schemas from the spec
|
||||
- `lib/api-client-react` — generated React Query hooks
|
||||
- `lib/db` — Drizzle schema + DB access
|
||||
- `artifacts/api-server` — Express backend
|
||||
- `artifacts/toolrate` — React frontend
|
||||
- `artifacts/mockup-sandbox` — component preview
|
||||
|
||||
## Run & develop
|
||||
|
||||
- `pnpm --filter @workspace/api-server run dev` — run the API server
|
||||
- `pnpm --filter @workspace/toolrate run dev` — run the frontend
|
||||
- `pnpm run typecheck` — full typecheck across all packages
|
||||
- `pnpm run build` — typecheck + build all packages
|
||||
- `pnpm --filter @workspace/api-spec run codegen` — regenerate API hooks and Zod schemas from the OpenAPI spec
|
||||
@@ -13,29 +33,16 @@ Community platform for listing and rating developer/productivity tools by useful
|
||||
|
||||
## Required env vars
|
||||
|
||||
- `DATABASE_URL` — Postgres connection string (auto-provisioned)
|
||||
- `SESSION_SECRET` — Session signing secret (already set)
|
||||
- `DATABASE_URL` — Postgres connection string
|
||||
- `SESSION_SECRET` — session signing secret
|
||||
- `VOTER_SECRET` — secret for voter submissions
|
||||
- `LOCAL_ADMIN_USERNAME` / `LOCAL_ADMIN_PASSWORD` — local admin login
|
||||
|
||||
## Keycloak (optional)
|
||||
## Auth
|
||||
|
||||
Set these to enable login:
|
||||
- `KEYCLOAK_URL` — e.g. `https://auth.example.com`
|
||||
- `KEYCLOAK_REALM` — realm name
|
||||
- `KEYCLOAK_CLIENT_ID` — client ID
|
||||
- `KEYCLOAK_CLIENT_SECRET` — client secret
|
||||
- `APP_URL` — public base URL for OAuth callback (optional, auto-detected if omitted)
|
||||
|
||||
Without these, the app runs in read-only mode (browsing and viewing ratings works, submitting tools/ratings requires login).
|
||||
|
||||
## Stack
|
||||
|
||||
- pnpm workspaces, Node.js 24, TypeScript 5.9
|
||||
- API: Express 5 + openid-client (Keycloak OIDC) + express-session + connect-pg-simple
|
||||
- DB: PostgreSQL + Drizzle ORM
|
||||
- Validation: Zod (`zod/v4`), `drizzle-zod`
|
||||
- API codegen: Orval (from OpenAPI spec)
|
||||
- Build: esbuild (CJS bundle)
|
||||
- Frontend: React 19 + Vite + TanStack Query + wouter + shadcn/ui + recharts
|
||||
- Session-based auth (not JWT), sessions stored in Postgres via connect-pg-simple.
|
||||
- Write operations (create/update/delete tools, submit ratings) require auth; reads are public.
|
||||
- Login via local admin user (`LOCAL_ADMIN_*`). Keycloak/OIDC support exists as an optional legacy path.
|
||||
|
||||
## Where things live
|
||||
|
||||
@@ -45,7 +52,7 @@ Without these, the app runs in read-only mode (browsing and viewing ratings work
|
||||
- `lib/api-zod/src/generated/` — generated Zod schemas (do not edit)
|
||||
- `artifacts/api-server/src/routes/` — Express route handlers
|
||||
- `artifacts/api-server/src/middleware/auth.ts` — `requireAuth` middleware
|
||||
- `artifacts/api-server/src/routes/auth.ts` — Keycloak OIDC login/callback/logout/me
|
||||
- `artifacts/api-server/src/routes/auth.ts` — login/logout/me
|
||||
- `artifacts/toolrate/src/pages/` — frontend pages
|
||||
- `artifacts/toolrate/src/components/` — shared components (layout, tool-card, category-combobox, feature-input)
|
||||
- `artifacts/toolrate/src/hooks/use-auth.ts` — auth state hook
|
||||
@@ -54,9 +61,9 @@ Without these, the app runs in read-only mode (browsing and viewing ratings work
|
||||
|
||||
- Contract-first: OpenAPI spec → codegen → typed hooks + Zod schemas. Never hand-write fetch calls.
|
||||
- Session-based auth (not JWT) — sessions stored in Postgres via connect-pg-simple.
|
||||
- Write operations (create/update/delete tools, submit ratings) require auth. Reads are public.
|
||||
- Write operations (create/update/delete tools, submit votes) require auth. Reads are public.
|
||||
- Category and feature autocomplete are client-side filtered against live API data (no separate index).
|
||||
- Grafana can consume `/api/analytics/*` endpoints directly via JSON datasource plugin.
|
||||
- Grafana can consume `/api/analytics/*` endpoints directly via the JSON datasource plugin.
|
||||
|
||||
## Product
|
||||
|
||||
@@ -66,16 +73,12 @@ Without these, the app runs in read-only mode (browsing and viewing ratings work
|
||||
- Analytics dashboard: top tools chart, category breakdown, score distribution histograms
|
||||
- Grafana integration: all `/api/analytics/*` endpoints return clean JSON
|
||||
|
||||
## User preferences
|
||||
## Deployment
|
||||
|
||||
_Populate as you build — explicit user instructions worth remembering across sessions._
|
||||
- Container image: `git.kubebase.de/admin/tool-evaluator:<tag>` (pushed by CI)
|
||||
- Deployed to k3s via the `apps` repo (kustomize), served at `https://toolr.kubebase.de`
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Orval clears the output folder during codegen — transient HMR errors in the dev server are normal and auto-recover.
|
||||
- Auth routes use PKCE — the code_verifier is stored in the session, not in-memory state.
|
||||
- `trust proxy: 1` is set on Express so that session cookies work correctly behind Replit's reverse proxy.
|
||||
|
||||
## Pointers
|
||||
|
||||
- See the `pnpm-workspace` skill for workspace structure, TypeScript setup, and package details
|
||||
- Auth uses PKCE — the `code_verifier` is stored in the session, not in-memory state.
|
||||
@@ -1,32 +0,0 @@
|
||||
kind = "api"
|
||||
previewPath = "/api" # TODO - should be excluded from preview in the first place
|
||||
title = "API Server"
|
||||
version = "1.0.0"
|
||||
id = "3B4_FFSkEVBkAeYMFRJ2e"
|
||||
|
||||
[[services]]
|
||||
localPort = 8080
|
||||
name = "API Server"
|
||||
paths = ["/api"]
|
||||
|
||||
[services.development]
|
||||
run = "pnpm --filter @workspace/api-server run dev"
|
||||
|
||||
[services.production]
|
||||
|
||||
[services.production.build]
|
||||
args = ["pnpm", "--filter", "@workspace/api-server", "run", "build"]
|
||||
|
||||
[services.production.build.env]
|
||||
NODE_ENV = "production"
|
||||
|
||||
[services.production.run]
|
||||
# we don't run through pnpm to make startup faster in production
|
||||
args = ["node", "--enable-source-maps", "artifacts/api-server/dist/index.mjs"]
|
||||
|
||||
[services.production.run.env]
|
||||
PORT = "8080"
|
||||
NODE_ENV = "production"
|
||||
|
||||
[services.production.health.startup]
|
||||
path = "/api/healthz"
|
||||
@@ -1,17 +0,0 @@
|
||||
kind = "design"
|
||||
previewPath = "/__mockup"
|
||||
title = "Canvas"
|
||||
version = "1.0.0"
|
||||
id = "XegfDyZt7HqfW2Bb8Ghoy"
|
||||
|
||||
[[services]]
|
||||
localPort = 8081
|
||||
name = "Component Preview Server"
|
||||
paths = ["/__mockup"]
|
||||
|
||||
[services.env]
|
||||
PORT = "8081"
|
||||
BASE_PATH = "/__mockup"
|
||||
|
||||
[services.development]
|
||||
run = "pnpm --filter @workspace/mockup-sandbox run dev"
|
||||
@@ -1,31 +0,0 @@
|
||||
kind = "web"
|
||||
previewPath = "/"
|
||||
title = "ToolRate — Tool-Bewertungsplattform"
|
||||
version = "1.0.0"
|
||||
id = "artifacts/toolrate"
|
||||
router = "path"
|
||||
|
||||
[[integratedSkills]]
|
||||
name = "react-vite"
|
||||
version = "1.0.0"
|
||||
|
||||
[[services]]
|
||||
name = "web"
|
||||
paths = [ "/" ]
|
||||
localPort = 26015
|
||||
|
||||
[services.development]
|
||||
run = "pnpm --filter @workspace/toolrate run dev"
|
||||
|
||||
[services.production]
|
||||
build = [ "pnpm", "--filter", "@workspace/toolrate", "run", "build" ]
|
||||
publicDir = "artifacts/toolrate/dist/public"
|
||||
serve = "static"
|
||||
|
||||
[[services.production.rewrites]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
|
||||
[services.env]
|
||||
PORT = "26015"
|
||||
BASE_PATH = "/"
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Link, useLocation } from "wouter";
|
||||
import { LayoutDashboard, Wrench, PlusCircle, BarChart3, LogIn, LogOut, ShieldCheck, AlertTriangle, Trash2, Bookmark, Search } from "lucide-react";
|
||||
import { LayoutDashboard, Wrench, BarChart3, LogIn, LogOut, ShieldCheck, AlertTriangle, Bookmark, Search } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useGetVersion, getGetVersionQueryKey } from "@workspace/api-client-react";
|
||||
@@ -38,10 +38,8 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||
const mainLinks = [
|
||||
{ href: "/", label: t("nav.home"), icon: LayoutDashboard },
|
||||
{ href: "/tools", label: t("nav.browseTools"), icon: Wrench },
|
||||
{ href: "/tools/new", label: t("nav.addTool"), icon: PlusCircle },
|
||||
{ href: "/analytics", label: t("nav.analytics"), icon: BarChart3 },
|
||||
...(hasFeature("watchlist") ? [{ href: "/watchlist", label: t("nav.watchlist"), icon: Bookmark }] : []),
|
||||
...(hasFeature("trash") ? [{ href: "/trash", label: t("nav.trash"), icon: Trash2 }] : []),
|
||||
];
|
||||
|
||||
const adminLinks = [
|
||||
|
||||
@@ -39,9 +39,9 @@ export function UserMenu() {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 px-3 py-2">
|
||||
<div className="flex items-center gap-3 px-3 py-2 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0 group-data-[collapsible=icon]:gap-0">
|
||||
<Skeleton className="h-8 w-8 rounded-full" />
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-4 w-24 group-data-[collapsible=icon]:hidden" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -51,12 +51,12 @@ export function UserMenu() {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full gap-2"
|
||||
className="w-full gap-2 group-data-[collapsible=icon]:w-auto group-data-[collapsible=icon]:gap-0 group-data-[collapsible=icon]:justify-center"
|
||||
onClick={() => login(location)}
|
||||
data-testid="button-login"
|
||||
>
|
||||
<LogIn className="w-4 h-4" />
|
||||
{t("auth.signIn")}
|
||||
<span className="group-data-[collapsible=icon]:hidden">{t("auth.signIn")}</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -72,7 +72,7 @@ export function UserMenu() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start gap-3 px-3 py-2 h-auto"
|
||||
className="w-full justify-start gap-3 px-3 py-2 h-auto group-data-[collapsible=icon]:w-auto group-data-[collapsible=icon]:gap-0 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-1"
|
||||
data-testid="button-user-menu"
|
||||
>
|
||||
<Avatar className="h-8 w-8">
|
||||
@@ -80,7 +80,7 @@ export function UserMenu() {
|
||||
{initials(displayName)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="min-w-0 flex-1 text-left">
|
||||
<span className="min-w-0 flex-1 text-left group-data-[collapsible=icon]:hidden">
|
||||
<span className="block text-sm font-medium truncate">{displayName}</span>
|
||||
<span className="block text-[11px] text-muted-foreground truncate">
|
||||
{user.email ?? tier}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/com
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Layout } from "@/components/layout";
|
||||
import { ToolCard } from "@/components/tool-card";
|
||||
import { Star, Wrench, MessageSquare, ArrowRight } from "lucide-react";
|
||||
import { Star, Wrench, MessageSquare, ArrowRight, Plus } from "lucide-react";
|
||||
import { Link } from "wouter";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -22,9 +22,17 @@ export default function Home() {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="space-y-8 pb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight mb-2">{t("home.welcome")}</h1>
|
||||
<p className="text-muted-foreground">{t("home.tagline")}</p>
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-end gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight mb-2">{t("home.welcome")}</h1>
|
||||
<p className="text-muted-foreground">{t("home.tagline")}</p>
|
||||
</div>
|
||||
<Button asChild>
|
||||
<Link href="/tools/new">
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
{t("browse.addTool")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats Banner */}
|
||||
|
||||
@@ -25,7 +25,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Search, Wrench, X, ArrowUp, ArrowDown, ArrowUpDown, SlidersHorizontal } from "lucide-react";
|
||||
import { Search, Wrench, X, ArrowUp, ArrowDown, ArrowUpDown, SlidersHorizontal, Plus } from "lucide-react";
|
||||
import { Link, useLocation, useSearch } from "wouter";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -234,7 +234,10 @@ export default function ToolsBrowse() {
|
||||
<p className="text-muted-foreground">{t("browse.subtitle")}</p>
|
||||
</div>
|
||||
<Button asChild>
|
||||
<Link href="/tools/new">{t("browse.addTool")}</Link>
|
||||
<Link href="/tools/new">
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
{t("browse.addTool")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm --filter db push
|
||||
Reference in New Issue
Block a user