Remove obsolete scripts and configuration files for self-hosting and database migrations

- Deleted `pnpm-workspace.yaml` as it is no longer needed.
- Removed `apply-migrations.sh` script that handled database migrations.
- Eliminated `generate-full-stack-env.mjs` script for environment variable generation.
- Deleted `selfhost-backup.sh` script for creating backups of the self-hosted environment.
- Removed `selfhost-doctor.sh` script for health checks of the self-hosted services.
- Deleted `selfhost-restore.sh` script for restoring backups in the self-hosted environment.
This commit is contained in:
Poyraz
2026-06-16 09:57:32 +03:00
parent da8c55d1ad
commit 72a0b38f3e
48 changed files with 2463 additions and 13020 deletions
-47
View File
@@ -1,47 +0,0 @@
# Dependencies
node_modules
npm-debug.log*
yarn-debug.log*
pnpm-debug.log*
# Next.js
.next
out
dist
build
# Environment files (will be injected at runtime)
.env*
!.env.example
# Version control
.git
.gitattributes
# IDE
.vscode
.idea
# OS
.DS_Store
Thumbs.db
# Python (legacy)
__pycache__
*.py
*.pyw
requirements.txt
# Docs & Scripts
docs
*.md
!README.md
*.bat
ROADMAP.md
# Supabase (not needed in Docker image)
supabase
# Artifacts / Gemini
.artifacts
.gemini
+6 -22
View File
@@ -1,30 +1,14 @@
# Neta self-host environment
# Use app-only when Neta connects to an external Supabase-compatible backend.
NETA_INSTALL_MODE=app-only
# Public URL where users open Neta. # Public URL where users open Neta.
NEXT_PUBLIC_SITE_URL=http://localhost:3000 NEXT_PUBLIC_SITE_URL=http://localhost:3000
# Host port mapped to the Neta container's internal port 3000. # Supabase project API URL, for example:
NETA_PORT=3000 # https://your-project-ref.supabase.co
# Supabase API URL. For a hosted project this looks like:
# https://your-project.supabase.co
# For a bundled/self-hosted Supabase setup this is usually your public API URL.
NEXT_PUBLIC_SUPABASE_URL= NEXT_PUBLIC_SUPABASE_URL=
# Supabase anon/public API key. # Supabase anon/public key.
NEXT_PUBLIC_SUPABASE_ANON_KEY= NEXT_PUBLIC_SUPABASE_ANON_KEY=
# Supabase service role key. Required for creating client portal accounts. # Supabase service role key. Required for creating the first admin,
# Keep this secret. It is only used server-side. # creating client portal users, and server-side storage uploads.
# Keep this secret. Never expose it with a NEXT_PUBLIC_ prefix.
SUPABASE_SERVICE_ROLE_KEY= SUPABASE_SERVICE_ROLE_KEY=
# Optional internal Supabase URL for Docker networks.
# In bundled mode this is set to http://neta-supabase-proxy:8000 by docker-compose.full.yml.
# SUPABASE_INTERNAL_URL=
# Optional: direct Postgres connection string used only by bash ./scripts/apply-migrations.sh.
# Do not expose this to browsers. It is not required by the web container.
# DATABASE_URL=postgresql://postgres:password@host:5432/postgres
-23
View File
@@ -1,23 +0,0 @@
# Neta full-stack self-host environment
NETA_INSTALL_MODE=full-stack
NEXT_PUBLIC_SITE_URL=http://localhost:3000
NETA_PORT=3000
# Public Supabase API URL reachable from the browser.
# For local Docker installs this is usually http://localhost:8000.
NEXT_PUBLIC_SUPABASE_URL=http://localhost:8000
SUPABASE_API_PORT=8000
# Generated by install.sh in full-stack mode.
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
JWT_SECRET=
POSTGRES_PASSWORD=
# Exposes bundled Postgres on the host for backups/manual access.
POSTGRES_PORT=54322
# Optional auth/mail settings.
JWT_EXPIRY=3600
SMTP_ADMIN_EMAIL=admin@neta.local
-17
View File
@@ -1,17 +0,0 @@
@echo off
setlocal
cd /d "%~dp0"
if not exist ".venv\Scripts\pythonw.exe" (
echo [INFO] Sanal ortam bulunamadi. Ilk kurulum baslatiliyor...
call "%~dp0kurulum.bat"
if not %errorlevel%==0 (
echo [HATA] Kurulum basarisiz. Program baslatilamadi.
pause
exit /b 1
)
)
call ".venv\Scripts\activate.bat"
start "AI Asistan" /B ".venv\Scripts\pythonw.exe" "%~dp0main.pyw"
exit /b 0
-63
View File
@@ -1,63 +0,0 @@
# ── Production Dockerfile (Dokploy / Coolify ready) ──────────────
FROM node:22-alpine AS base
# ── 1. Install dependencies ─────────────────────────────────────
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
RUN corepack enable pnpm
COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml* ./
RUN pnpm i --frozen-lockfile
# ── 2. Build ─────────────────────────────────────────────────────
FROM base AS builder
WORKDIR /app
RUN corepack enable pnpm
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js inlines NEXT_PUBLIC_* at build time.
# Dokploy / Coolify inject these as build-args automatically.
ARG NEXT_PUBLIC_SUPABASE_URL
ARG NEXT_PUBLIC_SUPABASE_ANON_KEY
ARG NEXT_PUBLIC_SITE_URL
ENV NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL
ENV NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
ENV NEXT_TELEMETRY_DISABLED=1
RUN pnpm build
# ── 3. Production runner ─────────────────────────────────────────
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy public assets
COPY --from=builder /app/public ./public
# Copy standalone output (Next.js output: "standalone")
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# Health check for Dokploy / Coolify / Docker orchestrators
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
CMD wget -qO- http://127.0.0.1:3000/api/health || exit 1
# Runtime env vars (Supabase keys, AI keys etc.) are injected by
# Dokploy / Coolify as environment variables — no .env file needed.
CMD ["node", "server.js"]
+51 -98
View File
@@ -1,130 +1,83 @@
<img src="public/logo/lightLogoLong.png" height="200" alt="Neta Icon" /> <img src="public/logo/lightLogoLong.png" height="200" alt="Neta Icon" />
# Neta - Freelancer Operating System # Neta
[![Next.js](https://img.shields.io/badge/Next.js-16.2.7-black?style=flat-square&logo=next.js)](https://nextjs.org/) Neta is a freelancer operating system built with Next.js and Supabase. It manages clients, projects, tasks, finance, daily logs, analytics, AI chat, and a limited client portal.
[![React](https://img.shields.io/badge/React-19.2.7-blue?style=flat-square&logo=react)](https://react.dev/)
[![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-4.3.0-38B2AC?style=flat-square&logo=tailwind-css)](https://tailwindcss.com/)
[![Supabase](https://img.shields.io/badge/Supabase-0.10.3-3ECF8E?style=flat-square&logo=supabase)](https://supabase.com/)
[![AI SDK](https://img.shields.io/badge/Vercel_AI_SDK-6.0.197-black?style=flat-square&logo=vercel)](https://sdk.vercel.ai/)
[![Poyraz UI](https://img.shields.io/badge/Poyraz_UI-2.1.0-indigo?style=flat-square)](https://github.com/poyrazavsever)
## Overview This repository now ships only the web application. It does not bundle Supabase, PostgreSQL, Docker Compose, installers, migration runners, or backup scripts. Bring your own Supabase project and provide the required environment variables in the deploy platform.
Neta is a comprehensive, self-hosted workspace designed specifically for freelancers. It serves as a centralized operating system to manage daily operations, clients, projects, finances, and personal performance tracking. ## Stack
Built with a focus on simplicity, security, and edge-to-edge aesthetic design, Neta allows a single user (the freelancer) to operate their entire business from one dashboard without relying on multiple third-party SaaS subscriptions. - Next.js App Router
- React
- Tailwind CSS
- Poyraz UI
- Supabase Auth, Postgres, Storage, and RLS
- Vercel AI SDK
## Features and Modules ## Requirements
### 1. Unified Dashboard 1. A Supabase project that already contains Neta's database schema, RLS policies, RPC functions, and storage buckets.
A command center providing a tactical overview of your current performance. It tracks active revenue, task completion, and upcoming deadlines, ensuring you always know what needs immediate attention. 2. Supabase project credentials:
- Project URL
- Anon/public key
- Service role key
3. Node.js 20 or newer.
### 2. Client Management See `docs/04-supabase-kurulumu.md` for the expected Supabase-side resources.
A structured CRM to manage your client base. You can track client statuses, store communication notes, and link clients directly to ongoing projects and tasks.
### 3. Project and Task Management ## Environment Variables
A robust pipeline for tracking project milestones and individual tasks. Features include priority assignments, status tracking, deadline management, and automated project progress calculations.
### 4. Financial Tracking Copy `.env.example` to `.env.local` for local development, or add the same values in Vercel, Coolify, Dokploy, or your hosting provider.
An integrated ledger to monitor income, expenses, and cash flow. It provides graphical analysis and clear tabular data of your recent transactions, allowing you to stay on top of your financial health.
### 5. Journal and Performance Analytics ```env
A dedicated journaling module to record daily mood, energy levels, and work satisfaction. This data is aggregated to provide trend analysis, helping you understand how your personal well-being correlates with your professional output. NEXT_PUBLIC_SITE_URL=https://your-domain.com
NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
### 6. Built-in AI Assistant NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
A localized AI module integrated directly into your workspace. It connects to your choice of provider (Google Gemini, OpenAI, Groq, or local Ollama instances) and can query your existing database to provide instant summaries, insights, and answers regarding your tasks and finances. SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
### 7. Client Portal
A restricted, read-only interface that you can share with your clients. They can log in to view the progress of their specific projects and public tasks, providing transparency without compromising your internal data security.
## Technical Architecture
Neta is engineered using modern, high-performance web technologies:
- **Frontend:** Next.js (App Router) combined with React Server Components.
- **Styling:** Tailwind CSS with a custom, bespoke design system via Poyraz UI.
- **Backend and Database:** Supabase (PostgreSQL) handling authentication, Row Level Security (RLS), and database migrations.
- **AI Integration:** Vercel AI SDK for streaming responses and tool-calling capabilities.
- **Deployment:** Fully containerized with Docker, optimized for standalone builds.
## Installation and Deployment
Neta is designed for self-hosting. It supports two Docker deployment modes:
- **Full-stack:** Neta + bundled Postgres/Auth/PostgREST/Storage.
- **App-only:** Neta connected to an existing Supabase-compatible backend.
### Prerequisites
- Docker and Docker Compose
- For app-only mode: Supabase API URL, anon key, service role key, and a direct Postgres `DATABASE_URL` for migrations
### 1-Click Installation (Recommended)
You can install Neta using the interactive setup script:
```bash
curl -fsSL https://raw.githubusercontent.com/poyrazavsever/neta/main/install.sh | bash
``` ```
The installer asks for the deployment mode, writes a `.env` file, validates Docker Compose configuration, and starts the application. In full-stack mode it generates Supabase JWT secrets and applies Neta migrations automatically through the `neta-migrations` service. `SUPABASE_SERVICE_ROLE_KEY` is server-only. Do not expose it with a `NEXT_PUBLIC_` prefix.
### Manual Installation ## Local Development
If you prefer to set up Neta manually:
1. Clone the repository: `git clone https://github.com/poyrazavsever/neta.git`
2. Navigate to the directory.
3. For full-stack mode, generate a `.env` file and run:
```bash ```bash
node scripts/generate-full-stack-env.mjs > .env npm install
npm run dev
``` ```
Open `http://localhost:3000`.
## Production Build
```bash ```bash
docker compose -f docker-compose.full.yml up -d --build npm run build
npm run start
``` ```
4. For app-only mode, copy `.env.example` to `.env`, fill every required value, and apply database migrations: ## Deploy
```bash ### Vercel
DATABASE_URL='postgresql://postgres:password@host:5432/postgres' sh ./scripts/apply-migrations.sh
```
5. Start the app-only Docker container: 1. Import the GitHub repository.
2. Add the environment variables from `.env.example`.
3. Deploy with the default Next.js settings.
```bash ### Coolify or Dokploy
docker compose up -d --build
```
Docker Compose intentionally fails fast when required Supabase environment values are missing. 1. Create a standard Next.js application from this GitHub repository.
2. Use the platform's normal install/build/start commands:
- Install: `npm install`
- Build: `npm run build`
- Start: `npm run start`
3. Add the environment variables from `.env.example`.
Coolify users should use `docker-compose.full.yml` for the no-external-service setup. Dokploy users should use `docker-compose.dokploy.yml`, which avoids fixed container names and host port bindings. Use `docker-compose.yml` only when connecting to an existing Supabase backend. See `docs/deployment/self-hosting.md` for the deployment checklist. No Dockerfile or Compose file is required.
### Operations ## First Admin
For full-stack installs, run health diagnostics and backups from the repository root: After deploying against a prepared Supabase project, open `/register` once to create the first freelancer/admin account. Registration is locked after the first admin profile exists.
```bash
sh ./scripts/selfhost-doctor.sh
sh ./scripts/selfhost-backup.sh
```
To also verify real Auth signup and password login, run:
```bash
NETA_DOCTOR_AUTH_SMOKE=1 sh ./scripts/selfhost-doctor.sh
```
Restore requires an existing backup directory and explicit confirmation:
```bash
sh ./scripts/selfhost-restore.sh ./backups/20260101T120000Z
```
### First Administrator Account
To ensure data security, Neta is locked to a single administrator. Upon launching the application for the first time, navigate to the `/register` route to create the initial admin account. Once this account is created, public registration is permanently disabled.
## License ## License
This project is proprietary and intended for personal self-hosting. This project is proprietary and intended for personal self-hosting with an external Supabase project.
+1 -1
View File
@@ -34,7 +34,7 @@ export async function signup(formData: FormData) {
if (!setupState.available) { if (!setupState.available) {
redirect( redirect(
`/login?error=true&message=${encodeURIComponent( `/login?error=true&message=${encodeURIComponent(
'Kayıt kapalı. Bu self-host kurulumunda ilk admin hesabı zaten oluşturulmuş.', 'Kayıt kapalı. Bu Neta kurulumunda ilk admin hesabı zaten oluşturulmuş.',
)}`, )}`,
) )
} }
+11 -19
View File
@@ -1,10 +1,9 @@
import { login } from "@/app/login/actions"; import { login } from "@/app/login/actions";
import { AuthPageShell } from "@/components/auth/auth-page-shell"; import { AuthPageShell } from "@/components/auth/auth-page-shell";
import { ErrorToaster } from "@/components/error-toaster"; import { ErrorToaster } from "@/components/error-toaster";
import { getFirstAdminSetupState } from "@/lib/auth/first-admin-setup";
import { LockKeyhole, LogIn, Mail } from "lucide-react"; import { LockKeyhole, LogIn, Mail } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { Button, Input, Label } from "poyraz-ui/atoms"; import { Input, Label } from "poyraz-ui/atoms";
import { SubmitButton } from "@/components/auth/submit-button"; import { SubmitButton } from "@/components/auth/submit-button";
export default async function LoginPage({ export default async function LoginPage({
@@ -12,10 +11,7 @@ export default async function LoginPage({
}: { }: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) { }) {
const [resolvedParams, setupState] = await Promise.all([ const resolvedParams = await searchParams;
searchParams,
getFirstAdminSetupState(),
]);
const error = resolvedParams?.error; const error = resolvedParams?.error;
const message = resolvedParams?.message; const message = resolvedParams?.message;
@@ -74,19 +70,15 @@ export default async function LoginPage({
} }
secondaryAction={null} secondaryAction={null}
footer={ footer={
setupState.available ? ( <div className="text-center text-sm">
<div className="text-center text-sm"> İlk kurulumu yapmadın mı?{" "}
İlk kurulumu yapmadın mı?{" "} <Link
<Link href="/register"
href="/register" className="font-medium text-primary transition-colors hover:text-primary-hover"
className="font-medium text-primary transition-colors hover:text-primary-hover" >
> Admin hesabını oluştur
Admin hesabını oluştur </Link>
</Link> </div>
</div>
) : (
<span></span>
)
} }
/> />
</> </>
+2 -2
View File
@@ -22,7 +22,7 @@ export default async function RegisterPage({
if (!setupState.available) { if (!setupState.available) {
redirect( redirect(
`/login?error=true&message=${encodeURIComponent( `/login?error=true&message=${encodeURIComponent(
"Kayıt kapalı. Bu self-host kurulumunda ilk admin hesabı zaten oluşturulmuş.", "Kayıt kapalı. Bu Neta kurulumunda ilk admin hesabı zaten oluşturulmuş.",
)}`, )}`,
); );
} }
@@ -36,7 +36,7 @@ export default async function RegisterPage({
{error && message && <ErrorToaster message={String(message)} />} {error && message && <ErrorToaster message={String(message)} />}
<AuthPageShell <AuthPageShell
title="İlk admin hesabını oluştur" title="İlk admin hesabını oluştur"
description="Bu self-host kurulumu için Neta çalışma alanının ilk yönetici hesabını oluştur." description="Bu Neta çalışma alanının ilk yönetici hesabını oluştur."
form={ form={
<form className="space-y-6"> <form className="space-y-6">
<div className="space-y-4"> <div className="space-y-4">
-42
View File
@@ -1,42 +0,0 @@
#!/usr/bin/env sh
set -eu
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<SQL
create role anon nologin;
create role authenticated nologin;
create role service_role nologin bypassrls;
create role authenticator noinherit login password '$POSTGRES_PASSWORD';
create role supabase_auth_admin noinherit login password '$POSTGRES_PASSWORD';
create role supabase_storage_admin noinherit login password '$POSTGRES_PASSWORD';
grant anon, authenticated, service_role to authenticator;
grant all privileges on database postgres to supabase_auth_admin;
grant all privileges on database postgres to supabase_storage_admin;
alter role supabase_auth_admin set search_path = auth, public;
alter role supabase_storage_admin set search_path = storage, public;
create schema if not exists auth authorization supabase_auth_admin;
create schema if not exists storage authorization supabase_storage_admin;
grant usage, create on schema public to supabase_auth_admin;
grant usage, create on schema public to supabase_storage_admin;
grant usage on schema public to anon, authenticated, service_role;
grant usage on schema auth to supabase_auth_admin;
grant usage on schema storage to anon, authenticated, service_role, supabase_storage_admin;
create extension if not exists "uuid-ossp";
create extension if not exists pgcrypto;
create extension if not exists vector;
do \$\$
begin
create type auth.factor_type as enum ('totp', 'webauthn');
exception
when duplicate_object then null;
end
\$\$;
alter type auth.factor_type owner to supabase_auth_admin;
alter default privileges in schema public grant select, insert, update, delete on tables to anon, authenticated, service_role;
alter default privileges in schema public grant usage, select on sequences to anon, authenticated, service_role;
SQL
@@ -1,37 +0,0 @@
server {
listen 8000;
server_name _;
client_max_body_size 50m;
location /auth/v1/ {
proxy_pass http://neta-auth:9999/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /rest/v1/ {
proxy_pass http://neta-rest:3000/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /storage/v1/ {
proxy_pass http://neta-storage:5000/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /health {
return 200 "ok\n";
add_header Content-Type text/plain;
}
}
-163
View File
@@ -1,163 +0,0 @@
services:
neta-web:
build:
context: .
dockerfile: Dockerfile
args:
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:?Set NEXT_PUBLIC_SUPABASE_URL in Dokploy env}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY:?Set NEXT_PUBLIC_SUPABASE_ANON_KEY in Dokploy env}
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}
restart: unless-stopped
depends_on:
neta-migrations:
condition: service_completed_successfully
neta-supabase-proxy:
condition: service_healthy
expose:
- "3000"
environment:
NODE_ENV: production
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:?Set NEXT_PUBLIC_SUPABASE_URL in Dokploy env}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY:?Set NEXT_PUBLIC_SUPABASE_ANON_KEY in Dokploy env}
SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY:?Set SUPABASE_SERVICE_ROLE_KEY in Dokploy env}
SUPABASE_INTERNAL_URL: http://neta-supabase-proxy:8000
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}
neta-supabase-proxy:
image: nginx:1.27-alpine
restart: unless-stopped
depends_on:
neta-auth:
condition: service_started
neta-rest:
condition: service_started
neta-storage:
condition: service_started
expose:
- "8000"
volumes:
- ./deploy/supabase/nginx/default.conf.template:/etc/nginx/templates/default.conf.template:ro
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8000/health >/dev/null"]
interval: 10s
timeout: 5s
retries: 12
neta-auth:
image: supabase/gotrue:v2.189.0
restart: unless-stopped
depends_on:
neta-db:
condition: service_healthy
environment:
GOTRUE_API_HOST: 0.0.0.0
GOTRUE_API_PORT: 9999
API_EXTERNAL_URL: ${NEXT_PUBLIC_SUPABASE_URL:?Set NEXT_PUBLIC_SUPABASE_URL in Dokploy env}/auth/v1
GOTRUE_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}
GOTRUE_URI_ALLOW_LIST: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}
GOTRUE_DB_DRIVER: postgres
GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in Dokploy env}@neta-db:5432/postgres
GOTRUE_JWT_SECRET: ${JWT_SECRET:?Set JWT_SECRET in Dokploy env}
GOTRUE_JWT_EXP: ${JWT_EXPIRY:-3600}
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
GOTRUE_DISABLE_SIGNUP: "true"
GOTRUE_EXTERNAL_EMAIL_ENABLED: "true"
GOTRUE_MAILER_AUTOCONFIRM: "true"
GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL:-admin@neta.local}
GOTRUE_SMTP_HOST: ${SMTP_HOST:-}
GOTRUE_SMTP_PORT: ${SMTP_PORT:-587}
GOTRUE_SMTP_USER: ${SMTP_USER:-}
GOTRUE_SMTP_PASS: ${SMTP_PASS:-}
GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME:-Neta}
neta-rest:
image: postgrest/postgrest:v12.2.12
restart: unless-stopped
depends_on:
neta-db:
condition: service_healthy
environment:
PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in Dokploy env}@neta-db:5432/postgres
PGRST_DB_SCHEMAS: public,storage
PGRST_DB_ANON_ROLE: anon
PGRST_JWT_SECRET: ${JWT_SECRET:?Set JWT_SECRET in Dokploy env}
PGRST_DB_USE_LEGACY_GUCS: "false"
neta-storage:
image: supabase/storage-api:v1.24.7
restart: unless-stopped
depends_on:
neta-db:
condition: service_healthy
neta-rest:
condition: service_started
environment:
ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY:?Set NEXT_PUBLIC_SUPABASE_ANON_KEY in Dokploy env}
SERVICE_KEY: ${SUPABASE_SERVICE_ROLE_KEY:?Set SUPABASE_SERVICE_ROLE_KEY in Dokploy env}
POSTGREST_URL: http://neta-rest:3000
PGRST_JWT_SECRET: ${JWT_SECRET:?Set JWT_SECRET in Dokploy env}
JWT_SECRET: ${JWT_SECRET:?Set JWT_SECRET in Dokploy env}
AUTH_JWT_SECRET: ${JWT_SECRET:?Set JWT_SECRET in Dokploy env}
SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY:?Set SUPABASE_SERVICE_ROLE_KEY in Dokploy env}
DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in Dokploy env}@neta-db:5432/postgres
FILE_SIZE_LIMIT: ${STORAGE_FILE_SIZE_LIMIT:-52428800}
STORAGE_BACKEND: file
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
TENANT_ID: stub
REGION: local
GLOBAL_S3_BUCKET: stub
ENABLE_IMAGE_TRANSFORMATION: "false"
volumes:
- neta-storage-data:/var/lib/storage
neta-migrations:
image: postgres:16-alpine
depends_on:
neta-db:
condition: service_healthy
neta-storage:
condition: service_started
environment:
NETA_MIGRATION_RUNNER_VERSION: "2026-06-14.3"
DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in Dokploy env}@neta-db:5432/postgres
NETA_POSTGREST_RELOAD_WAIT_SECONDS: ${NETA_POSTGREST_RELOAD_WAIT_SECONDS:-2}
volumes:
- ./supabase:/app/supabase:ro
- ./scripts:/app/scripts:ro
working_dir: /app
command:
- sh
- -c
- |
tries=0
until psql "$$DATABASE_URL" -v ON_ERROR_STOP=1 -tAc "select case when to_regclass('auth.users') is not null and to_regclass('storage.buckets') is not null then 'ready' else 'waiting' end" | grep -q "ready"; do
tries=$$((tries + 1))
if [ "$$tries" -gt 60 ]; then
echo "Timed out waiting for Supabase Auth/Storage schemas."
exit 1
fi
echo "Waiting for Supabase Auth/Storage schemas..."
sleep 2
done
sh ./scripts/apply-migrations.sh
neta-db:
image: pgvector/pgvector:pg16
restart: unless-stopped
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in Dokploy env}
POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
interval: 5s
timeout: 5s
retries: 20
volumes:
- neta-db-data:/var/lib/postgresql/data
- ./deploy/supabase/db/init.sh:/docker-entrypoint-initdb.d/00-neta-init.sh:ro
volumes:
neta-db-data:
neta-storage-data:
-172
View File
@@ -1,172 +0,0 @@
services:
neta-web:
build:
context: .
dockerfile: Dockerfile
args:
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:?Set NEXT_PUBLIC_SUPABASE_URL in .env}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY:?Set NEXT_PUBLIC_SUPABASE_ANON_KEY in .env}
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}
container_name: neta-web
restart: unless-stopped
depends_on:
neta-migrations:
condition: service_completed_successfully
neta-supabase-proxy:
condition: service_healthy
ports:
- "${NETA_PORT:-3000}:3000"
environment:
NODE_ENV: production
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:?Set NEXT_PUBLIC_SUPABASE_URL in .env}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY:?Set NEXT_PUBLIC_SUPABASE_ANON_KEY in .env}
SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY:?Set SUPABASE_SERVICE_ROLE_KEY in .env}
SUPABASE_INTERNAL_URL: http://neta-supabase-proxy:8000
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}
neta-supabase-proxy:
image: nginx:1.27-alpine
container_name: neta-supabase-proxy
restart: unless-stopped
depends_on:
neta-auth:
condition: service_started
neta-rest:
condition: service_started
neta-storage:
condition: service_started
ports:
- "${SUPABASE_API_PORT:-8000}:8000"
volumes:
- ./deploy/supabase/nginx/default.conf.template:/etc/nginx/templates/default.conf.template:ro
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8000/health >/dev/null"]
interval: 10s
timeout: 5s
retries: 12
neta-auth:
image: supabase/gotrue:v2.189.0
container_name: neta-auth
restart: unless-stopped
depends_on:
neta-db:
condition: service_healthy
environment:
GOTRUE_API_HOST: 0.0.0.0
GOTRUE_API_PORT: 9999
API_EXTERNAL_URL: ${NEXT_PUBLIC_SUPABASE_URL:?Set NEXT_PUBLIC_SUPABASE_URL in .env}/auth/v1
GOTRUE_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}
GOTRUE_URI_ALLOW_LIST: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}
GOTRUE_DB_DRIVER: postgres
GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}@neta-db:5432/postgres
GOTRUE_JWT_SECRET: ${JWT_SECRET:?Set JWT_SECRET in .env}
GOTRUE_JWT_EXP: ${JWT_EXPIRY:-3600}
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
GOTRUE_DISABLE_SIGNUP: "true"
GOTRUE_EXTERNAL_EMAIL_ENABLED: "true"
GOTRUE_MAILER_AUTOCONFIRM: "true"
GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL:-admin@neta.local}
GOTRUE_SMTP_HOST: ${SMTP_HOST:-}
GOTRUE_SMTP_PORT: ${SMTP_PORT:-587}
GOTRUE_SMTP_USER: ${SMTP_USER:-}
GOTRUE_SMTP_PASS: ${SMTP_PASS:-}
GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME:-Neta}
neta-rest:
image: postgrest/postgrest:v12.2.12
container_name: neta-rest
restart: unless-stopped
depends_on:
neta-db:
condition: service_healthy
environment:
PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}@neta-db:5432/postgres
PGRST_DB_SCHEMAS: public,storage
PGRST_DB_ANON_ROLE: anon
PGRST_JWT_SECRET: ${JWT_SECRET:?Set JWT_SECRET in .env}
PGRST_DB_USE_LEGACY_GUCS: "false"
neta-storage:
image: supabase/storage-api:v1.24.7
container_name: neta-storage
restart: unless-stopped
depends_on:
neta-db:
condition: service_healthy
neta-rest:
condition: service_started
environment:
ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY:?Set NEXT_PUBLIC_SUPABASE_ANON_KEY in .env}
SERVICE_KEY: ${SUPABASE_SERVICE_ROLE_KEY:?Set SUPABASE_SERVICE_ROLE_KEY in .env}
POSTGREST_URL: http://neta-rest:3000
PGRST_JWT_SECRET: ${JWT_SECRET:?Set JWT_SECRET in .env}
JWT_SECRET: ${JWT_SECRET:?Set JWT_SECRET in .env}
AUTH_JWT_SECRET: ${JWT_SECRET:?Set JWT_SECRET in .env}
SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY:?Set SUPABASE_SERVICE_ROLE_KEY in .env}
DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}@neta-db:5432/postgres
FILE_SIZE_LIMIT: ${STORAGE_FILE_SIZE_LIMIT:-52428800}
STORAGE_BACKEND: file
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
TENANT_ID: stub
REGION: local
GLOBAL_S3_BUCKET: stub
ENABLE_IMAGE_TRANSFORMATION: "false"
volumes:
- neta-storage-data:/var/lib/storage
neta-migrations:
image: postgres:16-alpine
container_name: neta-migrations
depends_on:
neta-db:
condition: service_healthy
neta-storage:
condition: service_started
environment:
NETA_MIGRATION_RUNNER_VERSION: "2026-06-14.3"
DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}@neta-db:5432/postgres
NETA_POSTGREST_RELOAD_WAIT_SECONDS: ${NETA_POSTGREST_RELOAD_WAIT_SECONDS:-2}
volumes:
- ./supabase:/app/supabase:ro
- ./scripts:/app/scripts:ro
working_dir: /app
command:
- sh
- -c
- |
tries=0
until psql "$$DATABASE_URL" -v ON_ERROR_STOP=1 -tAc "select case when to_regclass('auth.users') is not null and to_regclass('storage.buckets') is not null then 'ready' else 'waiting' end" | grep -q "ready"; do
tries=$$((tries + 1))
if [ "$$tries" -gt 60 ]; then
echo "Timed out waiting for Supabase Auth/Storage schemas."
exit 1
fi
echo "Waiting for Supabase Auth/Storage schemas..."
sleep 2
done
sh ./scripts/apply-migrations.sh
neta-db:
image: pgvector/pgvector:pg16
container_name: neta-db
restart: unless-stopped
ports:
- "${POSTGRES_PORT:-54322}:5432"
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
interval: 5s
timeout: 5s
retries: 20
volumes:
- neta-db-data:/var/lib/postgresql/data
- ./deploy/supabase/db/init.sh:/docker-entrypoint-initdb.d/00-neta-init.sh:ro
volumes:
neta-db-data:
neta-storage-data:
-20
View File
@@ -1,20 +0,0 @@
services:
neta-web:
build:
context: .
dockerfile: Dockerfile
args:
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:?Set NEXT_PUBLIC_SUPABASE_URL in .env}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY:?Set NEXT_PUBLIC_SUPABASE_ANON_KEY in .env}
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}
container_name: neta-web
restart: unless-stopped
ports:
- "${NETA_PORT:-3000}:3000"
environment:
NODE_ENV: production
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:?Set NEXT_PUBLIC_SUPABASE_URL in .env}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY:?Set NEXT_PUBLIC_SUPABASE_ANON_KEY in .env}
SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY:?Set SUPABASE_SERVICE_ROLE_KEY in .env}
SUPABASE_INTERNAL_URL: ${SUPABASE_INTERNAL_URL:-${NEXT_PUBLIC_SUPABASE_URL:?Set NEXT_PUBLIC_SUPABASE_URL in .env}}
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}
+72
View File
@@ -0,0 +1,72 @@
---
title: Giriş
description: Neta'nın ne olduğunu, kimler için tasarlandığını ve hangi problemi çözdüğünü anlatır.
order: 1
---
# Giriş
Neta'yı freelancer olarak çalışan birinin işlerini tek yerden yönetebilmesi için tasarladım. Gün içinde müşteri takibi, proje ilerlemesi, görevler, finansal durum, kişisel performans ve bazen de hızlıca soru sorabileceğim bir AI asistan ihtiyacı birbirinden kopuk araçlara dağılıyordu. Neta'nın çıkış noktası bu dağınıklığı azaltmak.
Bu proje bir ajans yönetim paneli olmaktan çok, bireysel çalışan bir freelancerın kendi operasyonunu düzenli tutması için hazırlanmış bir çalışma alanı. Temel fikir şu: müşterilerimi, projelerimi, görevlerimi, gelir-giderimi, günlük notlarımı ve müşteri portalını aynı sistem içinde görebileyim.
## Neta Ne İşe Yarar?
Neta ile şu işleri takip edebilirim:
- Müşterilerimi ve müşteri pipeline'ımı düzenli tutmak.
- Projelerimi durum, bütçe, ilerleme ve teslim tarihiyle yönetmek.
- Görevleri proje ve müşteriyle ilişkilendirmek.
- Gelir ve giderleri kaydetmek.
- Günlük ruh hâli, enerji ve iş memnuniyeti notları tutmak.
- Dashboard ve analiz ekranlarıyla genel gidişatı görmek.
- AI asistana mevcut verilerim hakkında soru sormak.
- Müşterilere sınırlı bir portal açıp proje ilerlemesini göstermek.
## Bu Repo Ne İçerir?
Bu repo artık sadece Neta web uygulamasını içerir. Uygulama Next.js ile geliştirilmiştir ve dışarıdan bağlanan bir Supabase projesiyle çalışır.
Repo içinde şunlar vardır:
- Next.js uygulama kodu.
- Dashboard, portal ve API route'ları.
- UI bileşenleri.
- Supabase bağlantı katmanı.
- Database migration notları ve SQL kayıtları.
- Deploy ve geliştirme dokümantasyonu.
Repo içinde artık şunlar yoktur:
- Supabase container'ları.
- PostgreSQL container'ı.
- Docker Compose deployment yapısı.
- Otomatik installer scriptleri.
- Backup/restore scriptleri.
Bu kararın sebebi basit: Neta'yı Vercel, Coolify veya Dokploy üzerinde standart bir Next.js uygulaması gibi yayınlamak istiyorum. Veritabanı ve auth tarafını ise Supabase üzerinde ayrıca yönetmek daha temiz ve daha anlaşılır bir kurulum sağlıyor.
## Kimler İçin Uygun?
Neta özellikle şu kişiler için uygun:
- Tek başına çalışan freelancerlar.
- Küçük ölçekli müşteri projeleri yöneten geliştiriciler veya tasarımcılar.
- Projelerini ve finansal durumunu aynı panelde görmek isteyen bağımsız çalışanlar.
- Müşterilerine basit bir proje takip portalı sunmak isteyenler.
- Kendi Supabase projesini yönetmekten çekinmeyen kullanıcılar.
Eğer çok kullanıcılı, ekip rolleri gelişmiş, kurumsal seviyede izin sistemi olan bir SaaS arıyorsanız Neta'nın mevcut hâli bunun için tasarlanmadı. Buradaki odak, tek ana kullanıcı ve onun müşterileri.
## Genel Akış
Kurulumdan sonra akış şu şekilde ilerler:
1. Supabase tarafında gerekli tablo, policy, function ve storage bucket'ları hazırlanır.
2. Uygulama repo olarak Vercel, Coolify veya Dokploy'a bağlanır.
3. Ortam değişkenleri girilir.
4. İlk admin hesabı `/register` üzerinden oluşturulur.
5. Dashboard üzerinden müşteri, proje, görev ve finans kayıtları eklenmeye başlanır.
6. İstenirse müşterilere portal hesabı oluşturulur.
Bu dokümantasyonun amacı sadece nasıl kurulur demek değil. Aynı zamanda projeyi neden böyle kurguladığımı, hangi parçanın ne işe yaradığını ve özelleştirmek isteyen birinin nereden başlaması gerektiğini netleştirmek.
+143
View File
@@ -0,0 +1,143 @@
---
title: Özellikler
description: Neta'nın freelancerlar için sunduğu temel özellikler.
order: 2
---
# Özellikler
Neta'yı parça parça özellik eklenmiş bir panel gibi değil, günlük iş akışımı toparlayan bir işletim alanı gibi düşünerek geliştirdim. Her modül tek başına işe yarıyor, ama asıl değer bu modüllerin birbirine bağlanmasında ortaya çıkıyor.
## Dashboard
Dashboard, Neta'yı açtığımda genel durumu hızlıca görebilmem için var. Burada gelir-gider özeti, aktif projeler, tamamlanan görevler, mood ve enerji trendleri gibi bilgiler yer alır.
Dashboard'un amacı detaylı işlem yapmak değil, bana şu soruların cevabını hızlıca vermek:
- Bu ay işlerin durumu nasıl?
- Aktif proje sayım ne?
- Son dönemde ne kadar gelir veya gider oluştu?
- Görev tamamlama ritmim nasıl?
- Kişisel performansım düşüyor mu, yükseliyor mu?
## Müşteri Yönetimi
Müşteri modülü, CRM'in sade bir versiyonu gibi çalışır. Burada müşteri adı, firma bilgisi, iletişim bilgileri, web sitesi, notlar ve pipeline aşaması tutulur.
Müşteriler şu aşamalarda takip edilebilir:
- Lead
- Contacted
- Proposal sent
- Won
- Lost
Böylece sadece aktif müşterileri değil, görüşme hâlindeki potansiyel işleri de takip etmek mümkün olur.
## Proje Yönetimi
Projeler Neta'nın ana omurgalarından biri. Her proje bir müşteriye bağlı olabilir ya da side project olarak bağımsız tutulabilir.
Bir projede şu bilgiler izlenir:
- Proje adı.
- Açıklama.
- Müşteri bağlantısı.
- Durum.
- Başlangıç ve teslim tarihi.
- Bütçe.
- Para birimi.
- İlerleme yüzdesi.
- Kapak görseli.
- Planlama bölümleri.
- Revizyon kotası.
Proje ilerlemesi manuel tutulabilir ya da görev durumlarına göre otomatik hesaplanabilir.
## Görev Yönetimi
Görevler proje ve müşteriyle ilişkilendirilebilir. Liste ve kanban görünümü vardır. Her görev için öncelik, durum, son tarih, tahmini süre ve gerçekleşen süre kaydedilebilir.
Görev durumları:
- `todo`
- `in_progress`
- `done`
Öncelikler:
- `low`
- `medium`
- `high`
- `urgent`
Müşteriye görünmesi istenen görevler ayrıca işaretlenebilir. Bu sayede client portal içinde sadece seçtiğim görevler gösterilir.
## Finans Takibi
Finans modülü gelir ve gider kayıtlarını tutar. Her işlem müşteri veya projeyle ilişkilendirilebilir.
Kaydedilebilen bilgiler:
- Gelir veya gider tipi.
- Tutar.
- Para birimi.
- İşlem tarihi.
- Kategori.
- Ödeme durumu.
- Açıklama.
Bu modül muhasebe programı yerine geçmek için değil, freelancer olarak işin finansal nabzını günlük seviyede görmek için tasarlandı.
## Günlük ve Kişisel Performans
Neta'da sadece operasyonel işler yok. Günlük modülüyle mood, enerji ve iş memnuniyeti skorları da tutulur.
Bu bölümü eklememin sebebi şu: Freelancer çalışırken iş yükü, motivasyon ve enerji birbirinden bağımsız değil. Proje yoğunluğu artarken enerji düşüyorsa bunu görmek değerli.
Günlük kaydı şunları içerir:
- Tarih.
- Mood skoru.
- Enerji skoru.
- İş memnuniyeti skoru.
- Not.
## Analizler
Analizler ekranı, finans ve görev verilerini görselleştirir. Burada proje bazlı gelir dağılımı ve görev durumları gibi özetler görülebilir.
Bu ekranı, detaylı BI aracı gibi değil, günlük kararları kolaylaştıran sade bir performans ekranı olarak düşündüm.
## AI Asistan
AI asistan, Neta içindeki veriler hakkında soru sormak için kullanılır. Örneğin:
- Bu ay finansal durumum nasıl?
- Hangi projeler riskli görünüyor?
- Yaklaşan görevlerim neler?
- Son günlük kayıtlarıma göre enerjim nasıl gidiyor?
AI asistan `app_settings` içindeki provider ve API key ayarlarını kullanır. OpenAI, Gemini ve Groq desteği vardır. Ayrıca projede yerel Ollama tabanlı Python yardımcı dosyası da ayrı bir kullanım senaryosu için korunur.
## Client Portal
Client portal, müşterilere sınırlı erişim vermek için var. Müşteri kendi hesabıyla giriş yapar ve sadece kendisine bağlı projeleri, public görevleri ve revizyon taleplerini görür.
Bu portalın amacı müşteriye tam dashboard açmak değil. Sadece şeffaflık sağlamak:
- Proje ilerlemesi.
- Teslim tarihi.
- Müşteriye açık görevler.
- Revizyon talepleri.
## Storage ve Dosya Kullanımı
Neta Supabase Storage kullanır.
Gerekli bucket'lar:
- `avatars`: Profil fotoğrafları için.
- `project-assets`: Proje kapak görselleri için.
Avatar bucket'ı public olabilir. Proje görselleri ise private tutulur ve uygulama gerektiğinde signed URL üretir.
+134
View File
@@ -0,0 +1,134 @@
---
title: Mimari
description: Neta'nın teknik mimarisi, frontend/backend ayrımı ve Supabase bağlantısı.
order: 3
---
# Mimari
Neta'yı klasik bir frontend-backend ayrımından çok, Next.js App Router etrafında şekillenen bir uygulama olarak kurguladım. Uygulamanın backend işlerinin büyük bölümü Supabase üzerinde duruyor. Next.js tarafı ise hem arayüzü hem de server action ve API route gibi uygulama mantığını taşıyor.
## Ana Teknolojiler
Projede temel olarak şunlar kullanılır:
- Next.js App Router.
- React Server Components.
- Client Components.
- Server Actions.
- Supabase Auth.
- Supabase Postgres.
- Supabase Storage.
- Supabase RLS.
- Vercel AI SDK.
- Tailwind CSS.
- Poyraz UI.
Bu yapı sayesinde ayrı bir Express/Nest backend yazmadan, server tarafı işler Next.js içinde yönetilebilir.
## Route Yapısı
Uygulama `app/` klasörü altında organize edilir.
Ana alanlar:
- `app/(dashboard)`: Freelancer dashboard ekranları.
- `app/portal`: Müşteri portalı.
- `app/login`: Giriş ekranı.
- `app/register`: İlk admin kayıt ekranı.
- `app/api`: AI ve yardımcı API route'ları.
Dashboard route'ları freelancer kullanıcısı için tasarlanmıştır. Portal route'ları ise `client` rolündeki kullanıcıları hedefler.
## Layout ve Rol Kontrolü
Dashboard layout içinde kullanıcı profili alınır. Eğer kullanıcının rolü `client` ise dashboard yerine `/portal` tarafına yönlendirilir.
Portal layout ise bunun tersini yapar. Kullanıcının rolü `client` değilse portal'a erişmesine izin verilmez.
Bu ayrım uygulama seviyesinde yapılır, ama asıl güvenlik Supabase RLS politikalarıyla sağlanır. Yani UI yanlışlıkla veri göstermeye çalışsa bile Supabase policy'leri yetkisiz veriyi döndürmemelidir.
## Supabase Client Katmanları
Projede üç farklı Supabase bağlantısı vardır:
- Browser client: Client component içinde kullanılır.
- Server client: Server component, server action ve route handler içinde kullanılır.
- Service role client: Sadece server tarafında, özel yetki gereken işlemler için kullanılır.
Service role client özellikle şu işler için gereklidir:
- İlk admin kullanıcısını oluşturmak.
- Client portal hesabı oluşturmak.
- Storage'a server-side upload yapmak.
Bu yüzden `SUPABASE_SERVICE_ROLE_KEY` kesinlikle client tarafına çıkarılmamalıdır.
## Middleware
`proxy.ts` içinde Supabase session güncellemesi yapılır. Giriş gerektiren route'larda kullanıcı yoksa `/login` sayfasına yönlendirilir.
Public auth route'ları bu kontrolden ayrı tutulur:
- `/login`
- `/register`
- `/auth`
- `/forgot-password`
Bu ayrım özellikle önemlidir. Login ekranının açılması için Supabase session doğrulamasını beklemek gereksiz bir blok yaratabilir. Bu yüzden public route'larda middleware dış servisi beklemeden geçer.
## Server Components ve Client Components
Neta'da genel desen şudur:
- Sayfa dosyası server component olarak veri çeker.
- UI ve etkileşim yoğun kısım client component'e aktarılır.
- CRUD işlemleri `actions.ts` dosyalarında server action olarak tutulur.
Örnek:
- `app/(dashboard)/tasks/page.tsx`: Veriyi çeker.
- `app/(dashboard)/tasks/tasks-client.tsx`: Liste, filtre, dialog ve kanban UI'ını yönetir.
- `app/(dashboard)/tasks/actions.ts`: Görev oluşturma, güncelleme ve silme işlemlerini yapar.
Bu desen projeyi okunabilir tutuyor.
## Veritabanı Güvenliği
Neta'da veri izolasyonunun ana mantığı `user_id` alanıdır. Freelancer verilerinde genellikle şu policy mantığı kullanılır:
```sql
auth.uid() = user_id
```
Müşteri portalı için ayrı bir ilişki vardır:
```txt
profiles.role = client
clients.client_auth_id = auth.users.id
```
Müşteri sadece kendi client kaydına bağlı projeleri ve public işaretlenmiş görevleri görebilir.
## AI Mimarisi
AI tarafında iki ana akış var:
- Chat ekranı: `/api/chat`
- Analiz endpoint'leri: `/api/finance-analysis`, `/api/project-risk`
Chat endpoint'i kullanıcının son görev, proje, finans ve günlük kayıtlarından bir bağlam oluşturur. Bu bağlam AI modele sistem mesajı olarak verilir.
Provider ayarları `app_settings` tablosundan okunur. Böylece kullanıcı OpenAI, Gemini veya Groq arasında seçim yapabilir.
## Docker Neden Yok?
Önceki denemelerde full-stack self-host yaklaşımı vardı. Bu yaklaşım Supabase, Postgres, Auth, Storage ve uygulamayı aynı deployment içine almaya çalışıyordu. Çalışır bir fikir olsa da kurulum ve bakım maliyeti fazlaydı.
Şimdiki hedef daha sade:
- Supabase dışarıda yönetilir.
- Neta sadece Next.js uygulaması olarak deploy edilir.
- Vercel, Coolify veya Dokploy standart akışları kullanılır.
Bu sayede proje daha az özel kurulum bilgisine ihtiyaç duyar ve GitHub reposundan deploy etmek kolaylaşır.
+121
View File
@@ -0,0 +1,121 @@
---
title: Supabase Kurulumu
description: Neta için gerekli Supabase tablo, policy, function ve storage gereksinimleri.
order: 4
---
# Supabase Kurulumu
Neta'nın çalışması için hazır bir Supabase projesine ihtiyaç var. Bu repo artık Supabase'i beraberinde getirmiyor. Yani uygulama deploy edilirken veritabanı, auth veya storage otomatik kurulmaz. Ben burada web uygulamasını sade tutmayı tercih ettim. Supabase tarafı ayrı bir altyapı olarak yönetilir.
> Şuan nestjs ile backend geliştiriyorum, çok yakında tam self host olacak :)
## Beklenen Kurulum
Kurulumda genel yaklaşım şu:
1. Supabase üzerinde bir proje oluşturulur.
2. Neta'nın ihtiyaç duyduğu SQL schema, policy, trigger ve function'lar Supabase tarafına uygulanır.
3. Gerekli storage bucket'ları oluşturulur.
4. Supabase URL ve key değerleri uygulama ortam değişkenlerine eklenir.
5. Uygulama deploy edilir.
Repo içinde `supabase/` ve `docs/database/` klasörleri tutulmaya devam eder. Bunları deployment sırasında otomatik çalıştıran bir script yoktur. Bu dosyalar benim SQL kayıtlarım, migration notlarım ve schema geçmişim olarak durur.
## Gerekli Tablolar
Neta şu public tabloları bekler:
- `profiles`
- `clients`
- `projects`
- `tasks`
- `calendar_events`
- `finance_transactions`
- `daily_logs`
- `app_settings`
- `chat_sessions`
- `chat_messages`
- `project_planning_sections`
- `project_revisions`
- `client_activities`
- `proposals`
- `contracts`
- `invoices`
- `subscriptions`
- `document_embeddings`
- `journals`
`journals` tablosu eski yapıdan gelen legacy bir tablodur. Güncel günlük ekranı ağırlıklı olarak `daily_logs` kullanır, ama schema geçmişinde `journals` hâlâ durur.
## Gerekli Function ve RPC'ler
Uygulama şu function/RPC yapılarına ihtiyaç duyar:
- `is_first_admin_setup_available`
- `request_internal_auth_creation`
- `match_documents`
- Yeni auth user için profile oluşturan trigger/function
- Proje ilerlemesini görev durumuna göre güncelleyen trigger/function
Özellikle `request_internal_auth_creation`, service role ile güvenli kullanıcı oluşturma akışında kullanılır. İlk admin ve müşteri portal kullanıcıları oluşturulurken bu yapı önemlidir.
## Row Level Security
Neta'da RLS açık olmalıdır. Freelancer verilerinin çoğu şu mantıkla ayrılır:
```sql
auth.uid() = user_id
```
Bu sayede her kullanıcı sadece kendi verisini görür.
Müşteri portalı ise farklı çalışır. Müşterinin auth kullanıcısı `clients.client_auth_id` alanına bağlanır. Portal erişimi bu ilişki üzerinden verilir.
Örnek mantık:
```txt
client auth user -> clients.client_auth_id -> projects.client_id
```
Müşteri sadece kendisine bağlı projeleri, public işaretli görevleri ve kendi revizyon taleplerini görmelidir.
## Storage Bucket'ları
Supabase Storage tarafında iki bucket beklenir.
| Bucket | Public | Dosya boyutu | MIME türleri |
| --- | --- | --- | --- |
| `avatars` | Evet | Supabase varsayılanı | Herhangi |
| `project-assets` | Hayır | 5 MB | `image/jpeg`, `image/png`, `image/webp`, `image/gif` |
`avatars` public olabilir çünkü profil fotoğrafları direkt gösterilir. `project-assets` private kalmalıdır. Proje görselleri için uygulama signed URL üretir.
## Service Role Key Neden Gerekli?
Uygulama normal kullanıcı işlemlerinde anon key ve session kullanır. Ama bazı işler server tarafında daha yüksek yetki gerektirir:
- İlk admin auth kullanıcısını oluşturmak.
- Müşteri portal kullanıcısı oluşturmak.
- Avatar veya proje görseli upload etmek.
- Bazı fallback setup kontrollerini yapmak.
Bu yüzden `SUPABASE_SERVICE_ROLE_KEY` zorunludur. Bu key asla browser'a gönderilmemelidir.
## SQL Dosyaları Nasıl Kullanılmalı?
Bu repodaki SQL dosyalarını otomatik migration sistemi gibi düşünmüyorum. Bunlar benim migration geçmişimi ve Supabase tarafında beklenen yapıyı belgeleyen kaynaklar.
Yeni bir kurulum yaparken en güvenilir kaynak, çalışan Supabase projesinden aldığınız güncel SQL çıktısıdır. Supabase panelindeki "copy as SQL" çıktısı bu yüzden önemlidir.
Eğer schema'yı elle hazırlıyorsanız:
1. Önce extension'ları açın.
2. Tabloları oluşturun.
3. Foreign key ve check constraint'leri ekleyin.
4. Function ve trigger'ları ekleyin.
5. RLS'i aktif edin.
6. Policy'leri ekleyin.
7. Storage bucket ve policy'leri ayarlayın.
Bu sıra bozulursa Supabase SQL editor bazı foreign key veya policy adımlarında hata verebilir.
+126
View File
@@ -0,0 +1,126 @@
---
title: Environment Ayarları
description: Neta'yı çalıştırmak için gereken ortam değişkenleri.
order: 5
---
# Environment Ayarları
Neta'nın çalışması için az sayıda ama kritik ortam değişkeni gerekiyor. Uygulama artık Supabase'i kendi içinde ayağa kaldırmadığı için bütün Supabase bağlantı bilgileri dışarıdan verilir.
Local geliştirme için `.env.example` dosyasını `.env.local` olarak kopyalayabilirsiniz. Production tarafında ise aynı değerleri Vercel, Coolify veya Dokploy panelinden environment variable olarak eklemek gerekir.
## Gerekli Değişkenler
```env
NEXT_PUBLIC_SITE_URL=https://your-domain.com
NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
```
## `NEXT_PUBLIC_SITE_URL`
Uygulamanın dışarıdan açıldığı URL'dir.
Local geliştirmede:
```env
NEXT_PUBLIC_SITE_URL=http://localhost:3000
```
Production'da:
```env
NEXT_PUBLIC_SITE_URL=https://neta.example.com
```
Bu değer özellikle link üretme, callback ve ileride e-posta akışları gibi noktalarda işe yarar.
## `NEXT_PUBLIC_SUPABASE_URL`
Supabase project API URL değeridir.
Supabase panelinde şu bölümden alınır:
```txt
Project Settings -> API -> Project URL
```
Format genellikle şöyledir:
```env
NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
```
Bu değer public olabilir. Browser tarafındaki Supabase client da bu URL'yi kullanır.
## `NEXT_PUBLIC_SUPABASE_ANON_KEY`
Supabase anon/public key değeridir.
Supabase panelinde şu bölümden alınır:
```txt
Project Settings -> API -> Project API keys -> anon public
```
Bu değer browser tarafında kullanılabilir. Zaten `NEXT_PUBLIC_` prefix'i bu yüzden vardır. Güvenlik bu key'in gizli olmasından değil, Supabase RLS policy'lerinin doğru yazılmış olmasından gelir.
## `SUPABASE_SERVICE_ROLE_KEY`
Bu en hassas değişkendir. Supabase service role key, RLS'i bypass edebilir. Bu yüzden asla `NEXT_PUBLIC_` prefix'iyle tanımlanmamalıdır.
Supabase panelinde şu bölümden alınır:
```txt
Project Settings -> API -> Project API keys -> service_role
```
Neta bu key'i sadece server tarafında kullanır.
Kullanıldığı yerler:
- İlk admin kullanıcısı oluşturma.
- Müşteri portal kullanıcısı oluşturma.
- Storage upload işlemleri.
- Bazı setup fallback kontrolleri.
## Local `.env.local` Örneği
```env
NEXT_PUBLIC_SITE_URL=http://localhost:3000
NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=ey...
SUPABASE_SERVICE_ROLE_KEY=ey...
```
`.env.local` dosyası repoya commit edilmemelidir.
## Production Env Kontrol Listesi
Deploy etmeden önce şunları kontrol ederim:
- `NEXT_PUBLIC_SUPABASE_URL` boş değil.
- `NEXT_PUBLIC_SUPABASE_ANON_KEY` doğru projeye ait.
- `SUPABASE_SERVICE_ROLE_KEY` doğru projeye ait.
- Production domain `NEXT_PUBLIC_SITE_URL` içine yazılmış.
- Supabase Auth tarafında site URL ve redirect URL ayarları production domain ile uyumlu.
## Yaygın Hatalar
### Login Açılıyor Ama Kayıt Oluşturulamıyor
Genellikle `SUPABASE_SERVICE_ROLE_KEY` eksiktir veya yanlış projeye aittir.
### Veriler Görünmüyor
Bu çoğu zaman env hatası değil, RLS policy hatasıdır. Kullanıcı session'ı doğru olsa bile policy izin vermiyorsa Supabase boş sonuç döndürür.
### Storage Upload Çalışmıyor
Bucket adı, bucket policy veya service role key kontrol edilmelidir.
### AI Asistan Çalışmıyor
AI provider ve API key ayarları uygulama içindeki Ayarlar ekranından kaydedilir. Ortam değişkeni olarak değil, `app_settings` tablosu üzerinden yönetilir.
+128
View File
@@ -0,0 +1,128 @@
---
title: Vercel ile Deploy
description: Neta'yı Vercel üzerinde yayınlama adımları.
order: 6
---
# Vercel ile Deploy
Neta'yı Vercel üzerinde yayınlamak en sade yollardan biri. Çünkü proje standart bir Next.js uygulaması olarak çalışır. Dockerfile, Compose veya özel installer gerekmez.
Benim önerdiğim akış şu: Supabase projesini önce hazırlayın, sonra GitHub reposunu Vercel'e bağlayın.
## Ön Hazırlık
Deploy'dan önce hazır olması gerekenler:
- GitHub üzerinde Neta reposu.
- Supabase projesi.
- Supabase tabloları, policy'leri, function'ları ve storage bucket'ları.
- Environment variable değerleri.
Gerekli env değerleri:
```env
NEXT_PUBLIC_SITE_URL=https://your-domain.com
NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
```
## Vercel'e Repo Bağlama
1. Vercel dashboard'a girin.
2. `Add New Project` seçin.
3. GitHub üzerinden Neta reposunu seçin.
4. Framework olarak Next.js otomatik algılanır.
5. Build ayarlarını varsayılan bırakabilirsiniz.
Vercel genelde şu komutları kendisi ayarlar:
```bash
npm install
npm run build
```
Start command girmeniz gerekmez. Vercel Next.js runtime'ını kendi yönetir.
## Environment Variable Ekleme
Vercel proje ayarlarında şu bölüme girin:
```txt
Project Settings -> Environment Variables
```
Değerleri production, preview ve development ortamlarına ihtiyaca göre ekleyin.
Önemli not:
`SUPABASE_SERVICE_ROLE_KEY` server-only kalmalıdır. Vercel'de normal environment variable olarak eklemek yeterlidir. `NEXT_PUBLIC_` prefix'i vermeyin.
## Supabase Auth Ayarları
Supabase tarafında şu ayarlar production domain ile uyumlu olmalıdır:
```txt
Authentication -> URL Configuration
```
Önerilen değerler:
```txt
Site URL: https://your-domain.com
Redirect URLs:
https://your-domain.com/**
```
Eğer preview deployment kullanıyorsanız Vercel preview domainlerini de eklemek isteyebilirsiniz.
## İlk Deploy Sonrası
Deploy tamamlandıktan sonra şu adımları izlerim:
1. Uygulamayı production domain üzerinden açarım.
2. `/api/health` endpoint'ini kontrol ederim.
3. `/register` sayfasından ilk admin hesabını oluştururum.
4. Login olurum.
5. Dashboard açılıyor mu bakarım.
6. Bir müşteri, proje ve görev ekleyerek Supabase bağlantısını test ederim.
7. Profil fotoğrafı veya proje görseli upload ederek storage policy'lerini kontrol ederim.
## İlk Admin
İlk admin hesabı `/register` üzerinden oluşturulur. İlk profil oluştuğu anda public kayıt akışı kapanır.
Bu davranış güvenlik için önemli. Neta kişisel self-host bir çalışma alanı olarak tasarlandığı için herkesin kayıt olup sisteme girmesini istemem.
## Vercel'de Dikkat Edilecekler
### Build Başarılı Ama Sayfa Veri Göstermiyor
Supabase env değerlerini kontrol edin. Yanlış Supabase projesine bağlanıyor olabilirsiniz.
### Register Çalışmıyor
`SUPABASE_SERVICE_ROLE_KEY` eksik veya yanlış olabilir. Ayrıca `request_internal_auth_creation` RPC'si Supabase tarafında bulunmalıdır.
### Storage Upload Hata Veriyor
Bucket adlarını ve storage policy'lerini kontrol edin:
- `avatars`
- `project-assets`
### AI Asistan Cevap Vermiyor
Uygulama içindeki Ayarlar ekranından AI provider ve API key kaydedilmelidir.
## Vercel İçin Kısa Kontrol Listesi
- GitHub repo bağlandı.
- Next.js framework algılandı.
- Env variable'lar girildi.
- Supabase Auth URL ayarları yapıldı.
- Supabase schema ve RLS hazır.
- Storage bucket'ları oluşturuldu.
- İlk admin oluşturuldu.
- Dashboard veri yazıp okuyabiliyor.
+135
View File
@@ -0,0 +1,135 @@
---
title: Coolify ile Deploy
description: Neta'yı bir VPS üzerinde Coolify kullanarak yayınlama.
order: 7
---
# Coolify ile Deploy
Coolify, kendi VPS'im üzerinde uygulama yayınlamak istediğimde pratik bir seçenek. Neta artık Docker Compose ile gelen full-stack bir paket olmadığı için Coolify üzerinde standart bir Next.js uygulaması olarak deploy edilebilir.
Buradaki temel fikir şu: Coolify sadece Neta web uygulamasını çalıştırır. Supabase ayrı bir servis olarak dışarıdadır.
## Ön Hazırlık
Coolify deploy için hazır olması gerekenler:
- Bir VPS.
- Kurulu ve çalışan Coolify.
- GitHub üzerinde Neta reposu.
- Harici Supabase projesi.
- Supabase env değerleri.
Supabase tarafında tablo, policy, function ve storage bucket'ları hazır olmalıdır.
## Uygulama Oluşturma
Coolify içinde yeni bir application oluştururken:
1. Kaynak olarak GitHub reposunu seçin.
2. Uygulama tipi olarak Node.js veya Next.js seçin.
3. Docker Compose seçmeyin.
4. Dockerfile'a ihtiyaç yoktur.
Kullanılacak komutlar:
```bash
npm install
npm run build
npm run start
```
Eğer Coolify framework'ü otomatik algılıyorsa bu komutları kendisi de önerebilir.
## Port Ayarı
Next.js production server varsayılan olarak `3000` portunda çalışır.
Coolify içinde internal port olarak şunu kullanabilirsiniz:
```txt
3000
```
Domain ve SSL yönlendirmesini Coolify üzerinden yapabilirsiniz.
## Environment Variables
Coolify application ayarlarında şu değerleri ekleyin:
```env
NEXT_PUBLIC_SITE_URL=https://neta.example.com
NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
```
`SUPABASE_SERVICE_ROLE_KEY` değeri gizli tutulmalıdır. Coolify secret/environment değişkeni olarak tanımlamak yeterlidir.
## Supabase Auth Ayarları
Supabase panelinde production domain'i tanımlayın:
```txt
Authentication -> URL Configuration
```
Örnek:
```txt
Site URL: https://neta.example.com
Redirect URLs:
https://neta.example.com/**
```
## Health Check
Neta'da basit bir health endpoint vardır:
```txt
/api/health
```
Coolify health check ayarında bu endpoint'i kullanabilirsiniz. Beklenen yanıt JSON olarak `status: ok` bilgisidir.
## İlk Admin Kurulumu
Deploy tamamlandıktan sonra:
1. Domain üzerinden uygulamayı açın.
2. `/register` sayfasına gidin.
3. İlk admin hesabını oluşturun.
4. Login olun.
İlk admin oluştuğunda kayıt akışı kapanır. Sonraki kullanıcılar ancak uygulama içinden müşteri portal hesabı olarak oluşturulabilir.
## Coolify'da Sık Karşılaşılan Durumlar
### Build Takılıyor
Önce Node.js versiyonunu kontrol edin. Node.js 20 veya üzeri kullanmak daha sağlıklı olur.
### Uygulama Açılıyor Ama Login Sonrası Veri Yok
Genellikle Supabase env değerleri yanlış projeyi gösteriyordur ya da RLS policy'leri doğru değildir.
### Register Hata Veriyor
Service role key eksik olabilir. Ayrıca Supabase tarafında `request_internal_auth_creation` function'ı bulunmalıdır.
### Görsel Upload Hata Veriyor
Storage bucket'ları ve policy'leri kontrol edin. `project-assets` private olmalı, `avatars` public olabilir.
## Coolify İçin Kısa Kontrol Listesi
- GitHub repo bağlandı.
- Docker Compose kullanılmadı.
- Install command `npm install`.
- Build command `npm run build`.
- Start command `npm run start`.
- Port `3000`.
- Env değerleri girildi.
- Domain ve SSL ayarlandı.
- Supabase Auth URL ayarları yapıldı.
- `/api/health` endpoint'i çalışıyor.
+117
View File
@@ -0,0 +1,117 @@
---
title: Dokploy ile Deploy
description: Neta'yı Dokploy üzerinde Docker Compose olmadan yayınlama.
order: 8
---
# Dokploy ile Deploy
Dokploy üzerinde Neta'yı yayınlarken artık özel Dockerfile veya Compose dosyasına ihtiyaç yok. Bu projeyi standart bir Next.js repo deploy'u gibi ele almak yeterli.
Benim önerdiğim yapı şu: Dokploy sadece web uygulamasını çalıştırır, Supabase ise dışarıdaki hazır proje olarak kullanılır.
## Gerekli Hazırlık
Başlamadan önce şunlar hazır olmalı:
- Dokploy kurulu bir VPS.
- GitHub üzerinde Neta reposu.
- Supabase projesi.
- Supabase schema ve storage yapısı.
- Ortam değişkenleri.
## Dokploy'da Uygulama Oluşturma
Dokploy içinde yeni bir uygulama oluştururken GitHub reposunu seçin. Uygulamayı Node.js/Next.js app olarak yapılandırın.
Önerilen komutlar:
```bash
npm install
npm run build
npm run start
```
Docker Compose dosyası seçmeyin. Bu repo artık Supabase'i veya Postgres'i container olarak yanında getirmez.
## Environment Variables
Dokploy environment bölümüne şu değerleri girin:
```env
NEXT_PUBLIC_SITE_URL=https://neta.example.com
NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
```
Burada en dikkat edilmesi gereken değer `SUPABASE_SERVICE_ROLE_KEY`. Bu key server tarafında kalmalıdır.
## Domain ve Port
Next.js production server varsayılan olarak `3000` portunda çalışır.
Dokploy tarafında:
- Internal port: `3000`
- Domain: kendi domaininiz
- SSL: Dokploy üzerinden aktif
şeklinde ayarlanabilir.
## Supabase Ayarları
Supabase Auth tarafında domain ayarını güncelleyin:
```txt
Authentication -> URL Configuration
```
Örnek:
```txt
Site URL: https://neta.example.com
Redirect URLs:
https://neta.example.com/**
```
## İlk Çalıştırma
Deploy tamamlandıktan sonra şu sırayla ilerleyin:
1. `/api/health` endpoint'ini açın.
2. Ana sayfaya girin.
3. `/register` üzerinden ilk admin hesabını oluşturun.
4. Login olun.
5. Müşteri, proje ve görev ekleyerek Supabase bağlantısını test edin.
## Dokploy'da Nelere Dikkat Edilmeli?
### Dockerfile Aramayın
Bu repo artık Dockerfile içermez. Dokploy'un Node.js/Next.js build akışını kullanmak daha doğru.
### Supabase Container Beklemeyin
Bu deployment Supabase'i ayağa kaldırmaz. Supabase dışarıda hazır olmalıdır.
### Env Değerleri Deploy'dan Önce Girilmeli
Build bazı env değerlerini okuyabilir. Bu yüzden env değerlerini deploy'dan sonra değil, deploy öncesinde eklemek daha sağlıklı olur.
### RLS Hataları Sessiz Görünebilir
Supabase policy izin vermediğinde uygulama bazen hata göstermek yerine boş liste gösterebilir. Veri görünmüyorsa önce RLS policy'lerini kontrol edin.
## Dokploy İçin Kısa Kontrol Listesi
- GitHub repo bağlandı.
- Uygulama Node.js/Next.js olarak oluşturuldu.
- Dockerfile/Compose kullanılmadı.
- Install command `npm install`.
- Build command `npm run build`.
- Start command `npm run start`.
- Port `3000`.
- Env değerleri girildi.
- Domain ve SSL aktif.
- Supabase Auth URL ayarları yapıldı.
+89
View File
@@ -0,0 +1,89 @@
---
title: İlk Admin Kurulumu
description: Neta'da ilk yönetici hesabının nasıl oluşturulduğu.
order: 9
---
# İlk Admin Kurulumu
Neta'yı kişisel bir freelancer çalışma alanı olarak düşündüğüm için sistemde herkesin serbestçe kayıt olmasını istemedim. Bu yüzden ilk admin oluşturulduktan sonra public kayıt akışı kapanır.
İlk kurulumun temel akışı şudur:
1. Uygulama Supabase'e bağlanır.
2. `/register` sayfası açılır.
3. İlk freelancer/admin hesabı oluşturulur.
4. `profiles` tablosunda ilk profil oluşur.
5. Sonraki public kayıt girişimleri engellenir.
## İlk Admin Nasıl Oluşturulur?
Deploy tamamlandıktan sonra şu adrese gidin:
```txt
/register
```
E-posta ve şifre girerek ilk hesabı oluşturun. Bu hesap freelancer/admin olarak kabul edilir.
Kayıt sırasında server tarafında service role kullanılır. Bu yüzden `SUPABASE_SERVICE_ROLE_KEY` ortam değişkeni doğru olmalıdır.
## Kayıt Neden Kapanıyor?
Neta'nın mevcut rol modeli şu şekilde:
- `freelancer`: Ana kullanıcı.
- `client`: Müşteri portal kullanıcısı.
Ana freelancer hesabı public kayıtla yalnızca ilk kez oluşturulur. Bundan sonra sistem herkesin kayıt olmasına izin vermez. Çünkü bu proje çok kullanıcılı açık bir SaaS olarak değil, kişisel self-host çalışma alanı olarak tasarlandı.
## Setup Kontrolü Nasıl Yapılır?
Uygulama ilk admin var mı diye Supabase tarafında şu function'ı kullanır:
```txt
is_first_admin_setup_available
```
Bu function genellikle `profiles` tablosunda kayıt var mı diye bakar.
Eğer function Supabase tarafında eksikse veya PostgREST schema cache henüz güncel değilse fallback kontrol devreye girebilir. Bunun için service role key gerekir.
## İlk Admin Oluşturulamıyorsa
Şunları kontrol edin:
- `SUPABASE_SERVICE_ROLE_KEY` doğru mu?
- Supabase URL doğru projeyi mi gösteriyor?
- `profiles` tablosu var mı?
- `request_internal_auth_creation` function'ı var mı?
- Auth trigger yeni kullanıcı için profile oluşturuyor mu?
- Supabase SQL schema güncel mi?
## Yanlış Kullanıcı İlk Admin Olduysa
Bu durumda dikkatli ilerlemek gerekir. Çünkü Supabase Auth ve public tablolar birlikte çalışır.
Kontrol edilecek yerler:
- Supabase Auth users.
- `profiles` tablosu.
- Kullanıcının `role` alanı.
Geliştirme ortamında yanlış ilk admin oluştuysa ilgili auth user ve profile kaydı silinip tekrar denenebilir. Production ortamında bunu yapmadan önce database backup almak daha güvenlidir.
## Müşteri Kullanıcıları Nasıl Oluşturulur?
Müşteri kullanıcıları public `/register` üzerinden oluşturulmaz. Freelancer paneli içinden bir müşteri kaydına portal hesabı açılır.
Bu işlem sonucunda:
- Supabase Auth içinde yeni client user oluşur.
- `profiles.role` değeri `client` olur.
- `clients.client_auth_id` bu auth user id'sine bağlanır.
Müşteri login olduğunda dashboard'a değil, `/portal` alanına yönlendirilir.
## Güvenlik Notu
İlk admin kurulumu tamamlandıktan sonra `/register` linki görünse bile kayıt denemesi server tarafında tekrar kontrol edilir. Yani sadece linki saklamak güvenlik önlemi değildir; asıl kontrol server action ve Supabase tarafındadır.
+140
View File
@@ -0,0 +1,140 @@
---
title: AI Asistan
description: Neta'nın AI sohbet ve analiz özelliklerinin nasıl çalıştığı.
order: 10
---
# AI Asistan
Neta'daki AI asistanı, uygulamanın içindeki veriler hakkında hızlıca soru sorabilmek için ekledim. Bu asistanın amacı genel bir chatbot olmak değil; görevler, projeler, finans kayıtları ve günlük notları üzerinden bana bağlama dayalı cevap verebilmesi.
## Chat Ekranı
AI sohbet ekranı şu route altında bulunur:
```txt
/chat
```
Bu ekranda konuşmalar session olarak tutulur.
Kullanılan tablolar:
- `chat_sessions`
- `chat_messages`
Her yeni sohbet için `chat_sessions` içinde bir kayıt açılır. Kullanıcı ve asistan mesajları `chat_messages` tablosunda saklanır.
## API Route
Chat yanıtları şu endpoint üzerinden üretilir:
```txt
/api/chat
```
Bu endpoint önce kullanıcıyı Supabase session üzerinden doğrular. Sonra kullanıcının son verilerinden kısa bir bağlam oluşturur.
Bağlama dahil edilen örnek veriler:
- Son görevler.
- Son projeler.
- Son 30 gün finans kayıtları.
- Son günlük kayıtlar.
Bu bağlam sistem mesajı içine eklenir. Böylece AI model sadece boş sohbet etmiyor, Neta içindeki güncel kayıtları da dikkate alıyor.
## AI Provider Ayarları
AI provider bilgisi `app_settings` tablosunda tutulur.
Desteklenen provider'lar:
- OpenAI
- Google Gemini
- Groq
- Ollama
Web uygulamasındaki ana AI chat akışı cloud provider'lara göre kurgulanmıştır. Ollama seçeneği schema'da ve bazı yardımcı yapılarda geçer, ama local Ollama kullanımının asıl pratiği `main.pyw` gibi masaüstü yardımcı script tarafındadır.
## Ayarlar Ekranı
AI ayarları uygulama içinde şu ekrandan yönetilir:
```txt
/settings
```
Buradan provider ve API key kaydedilir.
Kaydedilen alanlar:
- `ai_provider`
- `ai_model`
- `api_key`
Bu değerler `app_settings` tablosunda saklanır.
## Finans Analizi
Finans analizi için ayrı bir endpoint vardır:
```txt
/api/finance-analysis
```
Bu endpoint son 30 günün finans işlemlerini alır, gelir-gider toplamlarını hesaplar ve AI modelden kısa bir finansal yorum ister.
Bu özellik muhasebe tavsiyesi vermek için değil, freelancer olarak finansal gidişatı hızlıca yorumlamak için eklenmiştir.
## Proje Risk Analizi
Proje risk analizi endpoint'i:
```txt
/api/project-risk
```
Bu endpoint tek bir proje veya aktif projeler üzerinden risk ve durum raporu üretir.
Kullanılan bilgiler:
- Proje adı.
- Müşteri adı.
- Durum.
- Bütçe.
- İlerleme.
- Başlangıç ve bitiş tarihleri.
- Görev sayısı.
- Tamamlanan görev sayısı.
## Embedding ve pgvector
Projede `document_embeddings` tablosu ve `match_documents` function'ı için altyapı bulunur. Bu yapı, ileride daha gelişmiş RAG özellikleri için kullanılabilir.
İlgili yardımcı fonksiyonlar:
- `generateEmbedding`
- `saveDocumentEmbedding`
- `searchSimilarDocuments`
Bu yapı tam ürün akışının merkezinde değil, ama gelecekte döküman, not veya proje içeriği üzerinden daha güçlü arama ve bağlam üretme için hazır tutulur.
## Local Python/Ollama Yardımcısı
Repo içinde `main.pyw` dosyası da bulunur. Bu dosya web uygulamasından bağımsız bir masaüstü metin işleme yardımcısıdır.
Ne yapar?
- Seçili metni kopyalar.
- F8 ile menü açar.
- Ollama API üzerinden metin düzeltme, çeviri, özetleme gibi işler yapar.
Bu dosya Neta web deployment akışının parçası değildir. Vercel, Coolify veya Dokploy deploy'u için gerekli değildir. Ama lokal kullanım için korunur.
## AI İçin Dikkat Edilecekler
- API key'ler doğru provider'a ait olmalıdır.
- Provider seçimi ile model adı uyumlu olmalıdır.
- Kullanıcı verisi AI modele gönderildiği için hangi provider'ın kullanıldığı bilinçli seçilmelidir.
- Finansal, hukuki veya klinik kesin hüküm üretmemesi için sistem prompt'larında sınırlayıcı ifadeler kullanılmalıdır.
+144
View File
@@ -0,0 +1,144 @@
---
title: Client Portal
description: Müşteri portalının amacı, erişim modeli ve kullanım akışı.
order: 11
---
# Client Portal
Client portal, müşteriye sınırlı ve güvenli bir görünüm sunmak için var. Neta'nın ana dashboard'u freelancer içindir. Müşteriye bu dashboard'u açmak doğru olmaz. Bunun yerine müşteri sadece kendisini ilgilendiren proje, görev ve revizyon bilgilerini görür.
## Portalın Amacı
Müşteri portalı şu sorulara cevap verir:
- Projem hangi aşamada?
- Genel ilerleme ne durumda?
- Hangi görevler tamamlandı veya devam ediyor?
- Revizyon talebi gönderebilir miyim?
- Proje teslim tarihi ne?
Bu sayede müşteriyle sürekli manuel durum güncellemesi paylaşmak yerine, kontrollü bir şeffaflık sağlanır.
## Roller
Neta'da iki temel rol vardır:
- `freelancer`
- `client`
Freelancer ana paneli kullanır. Client ise portal alanını kullanır.
Rol bilgisi `profiles.role` alanında tutulur.
## Client Auth Bağlantısı
Müşteri portal erişimi için `clients` tablosunda şu alan kullanılır:
```txt
client_auth_id
```
Bu alan Supabase Auth içindeki müşteri kullanıcısının id değerini tutar.
İlişki şu şekildedir:
```txt
auth.users.id -> clients.client_auth_id -> projects.client_id
```
Bu bağlantı sayesinde müşteri sadece kendi client kaydına bağlı projeleri görebilir.
## Müşteri Hesabı Oluşturma
Müşteri public register ekranından kayıt olmaz. Freelancer paneli içinden bir müşteri kaydına portal hesabı oluşturulur.
Bu işlem sırasında:
1. Service role ile Supabase Auth user oluşturulur.
2. Kullanıcının profile rolü `client` yapılır.
3. `clients.client_auth_id` alanı yeni kullanıcıya bağlanır.
Bu akış için `SUPABASE_SERVICE_ROLE_KEY` gereklidir.
## Portal Route'ları
Portal route'ları `app/portal` altında durur.
Ana ekranlar:
- `/portal`
- `/portal/projects`
- `/portal/projects/[id]`
- `/portal/tasks`
- `/portal/revisions`
Portal layout, kullanıcının rolünü kontrol eder. Rol `client` değilse kullanıcı dashboard tarafına yönlendirilir.
## Müşteri Neleri Görebilir?
Müşteri şunları görebilir:
- Kendisine bağlı projeler.
- Proje adı, açıklama, durum, ilerleme ve teslim tarihi.
- Public olarak işaretlenmiş görevler.
- Proje planlama bölümleri.
- Kendi revizyon talepleri.
Müşteri şunları görmemelidir:
- Freelancer dashboard'u.
- Finans kayıtları.
- Diğer müşteriler.
- Diğer projeler.
- Private görevler.
- App settings.
- AI chat geçmişi.
## Public Task Mantığı
Görevlerde şu alan bulunur:
```txt
is_public_to_client
```
Bu alan `true` ise müşteri portalında gösterilebilir. Böylece iç operasyon görevleri müşteriyle paylaşılmadan tutulabilir.
## Revizyon Talepleri
Müşteri proje detay ekranından revizyon talebi oluşturabilir.
Revizyon durumları:
- `pending`
- `in_progress`
- `completed`
- `rejected`
Freelancer dashboard tarafında bu talepler görülebilir ve durumları güncellenebilir.
## Güvenlik Modeli
Portal güvenliği iki katmanda sağlanır:
1. Next.js layout seviyesinde rol kontrolü.
2. Supabase RLS policy seviyesinde veri erişim kontrolü.
Sadece layout kontrolüne güvenmek doğru değildir. Asıl kritik nokta Supabase policy'lerinin doğru olmasıdır.
## Portalı Özelleştirme
Portal menüsü şu dosyadan yönetilir:
```txt
config/portal-sidebar.ts
```
Portal shell bileşeni:
```txt
components/layout/portal-shell.tsx
```
Müşteriye daha fazla bilgi göstermek isterseniz önce RLS politikasını, sonra server component veri sorgusunu, en son UI bileşenini güncellemek gerekir.
+122
View File
@@ -0,0 +1,122 @@
---
title: Storage ve Dosyalar
description: Avatar ve proje görselleri için Supabase Storage kullanımı.
order: 12
---
# Storage ve Dosyalar
Neta'da dosya saklama için Supabase Storage kullanılır. Şu an iki temel dosya ihtiyacı var: profil fotoğrafları ve proje kapak görselleri.
Bu dosyaları uygulama reposu içinde tutmuyorum. Çünkü production ortamında dosya upload'larının kalıcı ve güvenli bir storage alanında durması gerekir. Supabase Storage bu iş için yeterli ve pratik bir çözüm sağlıyor.
## Gerekli Bucket'lar
Neta iki bucket bekler:
| Bucket | Public | Kullanım |
| --- | --- | --- |
| `avatars` | Evet | Profil fotoğrafları |
| `project-assets` | Hayır | Proje kapak görselleri |
## `avatars` Bucket
Profil fotoğrafları için kullanılır.
Önerilen ayar:
- Bucket adı: `avatars`
- Public: Evet
- MIME kısıtı: Gerekirse image türleriyle sınırlandırılabilir
Avatar görselleri public URL ile gösterilir. Bu yüzden bucket public olabilir.
Profil fotoğrafı upload işlemi Ayarlar ekranından yapılır:
```txt
/settings
```
Server action tarafında service role client kullanılır ve upload sonrası public URL profile kaydına yazılır.
## `project-assets` Bucket
Proje kapak görselleri için kullanılır.
Önerilen ayar:
- Bucket adı: `project-assets`
- Public: Hayır
- File size limit: 5 MB
- Allowed MIME types:
- `image/jpeg`
- `image/png`
- `image/webp`
- `image/gif`
Bu bucket private kalmalıdır. Proje detay sayfasında görsel gerektiğinde server tarafında signed URL üretilir.
## Dosya Yolları
Proje görselleri şu mantıkla saklanır:
```txt
{user_id}/projects/{project_id}/{file_name}
```
Bu yapı storage policy yazmayı kolaylaştırır. Çünkü dosya yolunun ilk segmenti kullanıcının id değeridir.
## Signed URL Kullanımı
Private bucket içindeki proje görselleri doğrudan public URL ile gösterilmez. Bunun yerine server tarafında kısa süreli signed URL üretilir.
Bu yaklaşım şu avantajları sağlar:
- Proje dosyaları herkese açık olmaz.
- URL süresi sınırlıdır.
- Yetkisiz kullanıcılar dosya yolunu bilse bile doğrudan erişemez.
## Service Role Neden Kullanılıyor?
Upload işlemlerinde service role client kullanılır. Bunun sebebi storage policy karmaşıklığını azaltmak ve server-side işlemleri güvenli tutmaktır.
Yine de service role key sadece server tarafında kalmalıdır.
## Storage Policy Mantığı
Policy'ler şu iki durumu desteklemelidir:
- Kullanıcı kendi klasöründeki dosyaları yönetebilmeli.
- Service role server-side işlemler için erişebilmeli.
`project-assets` için tipik yol kontrolü:
```txt
auth.uid()::text = storage.foldername(name)[1]
```
Service role için ayrıca bypass izni verilmelidir.
## Sık Karşılaşılan Hatalar
### Upload Başarısız
Kontrol edilecekler:
- Bucket adı doğru mu?
- Bucket var mı?
- MIME type izinli mi?
- Dosya boyutu limiti aşılıyor mu?
- Service role key doğru mu?
### Görsel Yükleniyor Ama Görünmüyor
`project-assets` private olduğu için public URL beklemeyin. Signed URL üretimi çalışıyor mu kontrol edin.
### Avatar Görünmüyor
`avatars` bucket public değilse public URL görüntülenmeyebilir. Ya bucket public yapılmalı ya da avatar için de signed URL yaklaşımı kullanılmalıdır.
## Production Notu
Storage ayarları deploy platformundan bağımsızdır. Vercel, Coolify veya Dokploy fark etmez; dosya tarafındaki davranışı Supabase bucket ve policy ayarları belirler.
+175
View File
@@ -0,0 +1,175 @@
---
title: Geliştirme
description: Neta üzerinde lokal geliştirme yapma ve kod yapısını anlama.
order: 13
---
# Geliştirme
Neta'yı geliştirmek için önce standart bir Next.js uygulaması gibi düşünmek yeterli. Uygulama Supabase'e dışarıdan bağlanır; local geliştirme sırasında da aynı şekilde bir Supabase projesine ihtiyaç vardır.
## Kurulum
Önce bağımlılıkları kurun:
```bash
npm install
```
Sonra `.env.example` dosyasını `.env.local` olarak kopyalayıp değerleri doldurun:
```bash
cp .env.example .env.local
```
Windows PowerShell için:
```powershell
Copy-Item .env.example .env.local
```
Dev server:
```bash
npm run dev
```
Uygulama varsayılan olarak şurada açılır:
```txt
http://localhost:3000
```
## Klasör Yapısı
Ana klasörler:
```txt
app/
components/
config/
hooks/
lib/
public/
docs/
supabase/
```
## `app/`
Next.js App Router route'ları burada durur.
Önemli alanlar:
- `app/(dashboard)`: Freelancer dashboard ekranları.
- `app/portal`: Müşteri portalı.
- `app/api`: API route'ları.
- `app/login`: Login ekranı.
- `app/register`: İlk admin kayıt ekranı.
## `components/`
Paylaşılan UI bileşenleri burada durur.
Örnekler:
- Layout shell bileşenleri.
- Auth bileşenleri.
- UI yardımcıları.
- Dashboard hızlı aksiyon bileşenleri.
## `config/`
Menü ve navigation gibi sabit konfigürasyonlar burada durur.
Önemli dosyalar:
```txt
config/sidebar.ts
config/portal-sidebar.ts
```
Yeni bir dashboard sayfası eklerken genelde `sidebar.ts` dosyasını da güncellemek gerekir.
## `lib/`
Uygulama yardımcıları ve servis bağlantıları burada durur.
Önemli dosyalar:
- `lib/supabase/client.ts`
- `lib/supabase/server.ts`
- `lib/supabase/admin.ts`
- `lib/auth/first-admin-setup.ts`
- `lib/auth/internal-users.ts`
- `lib/ai.ts`
- `lib/ai/embeddings.ts`
## Veri Çekme Deseni
Projede sık kullandığım desen şu:
1. `page.tsx` server component olarak Supabase'ten veriyi çeker.
2. Client tarafında etkileşim gerekiyorsa veri bir `*-client.tsx` bileşenine aktarılır.
3. Form işlemleri `actions.ts` içinde server action olarak yapılır.
Örnek:
```txt
app/(dashboard)/tasks/page.tsx
app/(dashboard)/tasks/tasks-client.tsx
app/(dashboard)/tasks/actions.ts
```
Bu desen hem okunabilirliği artırır hem de Supabase sorgularının nerede çalıştığını netleştirir.
## Yeni Modül Eklemek
Yeni bir modül eklerken şu sırayı izlemek mantıklı:
1. Supabase tablosunu ve RLS policy'lerini planla.
2. Server page dosyasını oluştur.
3. Gerekirse client component oluştur.
4. CRUD işlemleri için `actions.ts` ekle.
5. Sidebar config'e menü ekle.
6. Gerekirse dashboard/analiz ekranına özet veri ekle.
## Build
Production build kontrolü:
```bash
npm run build
```
Build başarılıysa TypeScript ve Next.js route üretimi de temel seviyede doğrulanmış olur.
## Lint
Lint komutu:
```bash
npm run lint
```
Eğer lint yavaş çalışıyorsa `.next` klasörünün temiz olduğundan emin olun. Generated dosyaların gereksiz taranması geliştirme deneyimini bozabilir.
## `.next` Cache Temizleme
Dev server bazen eski route type veya cache nedeniyle garip davranabilir. Bu durumda:
```powershell
Remove-Item .next -Recurse -Force
```
Sonra tekrar:
```bash
npm run dev
```
## Python Yardımcı Dosyası
Repo içinde `main.pyw` ve `requirements.txt` bulunur. Bunlar web uygulamasının deploy akışına dahil değildir. Lokal Ollama tabanlı masaüstü metin işleme yardımcısı olarak korunur.
Python bağımlılıkları web build'i için gerekli değildir.
+191
View File
@@ -0,0 +1,191 @@
---
title: Özelleştirme
description: Neta'nın marka, arayüz, modül ve iş akışı açısından nasıl özelleştirileceği.
order: 14
---
# Özelleştirme
Neta'yı kendi kullanım şeklime göre geliştirdiğim için özelleştirilebilir kalmasına dikkat ettim. Renkler, logo, menüler, modüller, AI prompt'ları ve portal görünümü ihtiyaçlara göre değiştirilebilir.
## Logo Değiştirme
Logo dosyaları `public/logo` altında durur.
Kullanılan dosyalar:
```txt
public/logo/lightLogoLong.png
public/logo/blackLogoLong.png
public/logo/iconLogo.png
```
Logo kullanılan ana yerler:
- Auth ekranı.
- Dashboard sidebar.
- Portal sidebar.
- Mobil header.
Logo değiştirirken aynı dosya adlarını korumak en kolay yoldur. Dosya adlarını değiştirirseniz ilgili `Image` kullanımlarını da güncellemeniz gerekir.
## Renk ve Tema
Ana renk değişkenleri `app/globals.css` içinde tanımlanır.
Özellikle şu CSS değişkenleri önemlidir:
```css
--poyraz-background
--poyraz-foreground
--poyraz-primary
--poyraz-primary-foreground
--poyraz-border
--poyraz-muted
```
Tailwind tarafındaki eşlemeler `tailwind.config.ts` içinde bulunur.
Neta şu an açık tema odaklı tasarlanmıştır. Dark mode altyapısı tamamen ürünleşmiş kabul edilmemelidir.
## Sidebar Menüleri
Dashboard menüsü:
```txt
config/sidebar.ts
```
Portal menüsü:
```txt
config/portal-sidebar.ts
```
Yeni bir ekran eklediğinizde önce route'u oluşturun, sonra ilgili sidebar config'e menü elemanı ekleyin.
Örnek dashboard menü elemanı:
```ts
{ title: "Projeler", href: "/projects", icon: FolderKanban }
```
## Dashboard Özelleştirme
Dashboard ekranı iki parçadan oluşur:
```txt
app/(dashboard)/page.tsx
app/(dashboard)/dashboard-client.tsx
```
Server component veriyi toplar. Client component KPI ve grafik görünümünü üretir.
Yeni KPI eklemek için:
1. `page.tsx` içinde gerekli veriyi çekin.
2. `dashboardData` içine ekleyin.
3. `DashboardClient` tipini güncelleyin.
4. UI içinde yeni kartı oluşturun.
## Yeni Alan Eklemek
Bir tabloya yeni field eklemek istediğinizde sadece formu güncellemek yetmez.
Genel sıra:
1. Supabase tablosuna field ekle.
2. RLS etkisini kontrol et.
3. Server query select listesine field ekle.
4. TypeScript tiplerini güncelle.
5. Form input'unu ekle.
6. `actions.ts` payload okuma fonksiyonunu güncelle.
7. Liste/detay UI'ında göster.
Bu sırayı izlemek runtime hatalarını azaltır.
## AI Prompt Özelleştirme
AI chat prompt'u şu dosyada bulunur:
```txt
app/api/chat/route.ts
```
Finans analizi:
```txt
app/api/finance-analysis/route.ts
```
Proje risk analizi:
```txt
app/api/project-risk/route.ts
```
Prompt'ları değiştirirken dikkat ettiğim noktalar:
- Cevap dili Türkçe olmalı.
- Gereksiz uzun cevap istenmemeli.
- Veri yoksa model bunu açıkça söylemeli.
- Finansal, hukuki veya klinik kesin hüküm verilmemeli.
## Client Portal Özelleştirme
Portal shell:
```txt
components/layout/portal-shell.tsx
```
Portal sayfaları:
```txt
app/portal
```
Müşteriye yeni bir veri göstermek için sadece UI eklemek yetmez. Supabase RLS policy'sinin de o veriye izin vermesi gerekir.
Önerilen sıra:
1. Müşteri gerçekten bu veriyi görmeli mi karar ver.
2. RLS policy ekle veya güncelle.
3. Portal server component içinde sorguyu yaz.
4. Client component içinde göster.
## Yeni Modül Eklemek
Yeni modül için örnek klasör yapısı:
```txt
app/(dashboard)/new-module/page.tsx
app/(dashboard)/new-module/new-module-client.tsx
app/(dashboard)/new-module/actions.ts
```
Eğer modül müşteriyle ilişkiliyse `client_id`, projeyle ilişkiliyse `project_id` alanlarını baştan düşünmek iyi olur.
## Metinleri Türkçeleştirme
Projede bazı eski dosyalarda karakter bozulmaları veya İngilizce metinler kalmış olabilir. Ürünleştirme sırasında bu metinlerin tek tek düzeltilmesi gerekir.
Öncelikli yerler:
- Sidebar başlıkları.
- Form label'ları.
- Toast mesajları.
- Empty state metinleri.
- Hata mesajları.
## Kişisel Kullanıma Göre Sadeleştirme
Neta'yı herkes aynı şekilde kullanmak zorunda değil. Eğer bazı modüller size gereksizse menüden kaldırabilir, route'ları bırakıp görünürlüğü azaltabilirsiniz.
Örneğin finans modülünü kullanmayacaksanız:
- Sidebar'dan kaldırın.
- Dashboard finans kartlarını kaldırın.
- AI context içinde finans sorgularını sadeleştirin.
Bu yaklaşım projeyi daha kişisel ve odaklı tutar.
+173
View File
@@ -0,0 +1,173 @@
---
title: Sorun Giderme
description: Kurulum, deploy, Supabase ve giriş problemlerinde kontrol edilmesi gerekenler.
order: 15
---
# Sorun Giderme
Neta bir Next.js uygulaması gibi deploy edilse de Supabase, RLS, Auth ve Storage bağlantıları olduğu için sorunları doğru katmanda aramak önemli. Bu sayfada en sık karşılaşılabilecek durumları topladım.
## Login Sayfası Açılmıyor
Önce `.next` cache'ini temizleyin:
```powershell
Remove-Item .next -Recurse -Force
```
Sonra dev server'ı tekrar başlatın:
```bash
npm run dev
```
Eğer sayfa hâlâ açılmıyorsa şunları kontrol edin:
- `NEXT_PUBLIC_SUPABASE_URL` dolu mu?
- `NEXT_PUBLIC_SUPABASE_ANON_KEY` doğru mu?
- Supabase projesi erişilebilir mi?
- Dev server loglarında route compile hatası var mı?
Login sayfası public route olduğu için session kontrolünün sayfayı bloklamaması gerekir.
## Giriş Yapıyorum Ama Dashboard Açılmıyor
Kontrol edilecekler:
- Kullanıcının `profiles` tablosunda kaydı var mı?
- `profiles.role` değeri `freelancer` mı?
- Kullanıcı yanlışlıkla `client` rolünde mi?
- Middleware kullanıcıyı `/portal` tarafına mı yönlendiriyor?
Client rolündeki kullanıcı dashboard'a değil, portal'a yönlendirilir.
## İlk Admin Oluşturulamıyor
Bu genellikle service role veya Supabase function problemidir.
Kontrol edin:
- `SUPABASE_SERVICE_ROLE_KEY` var mı?
- Service role key doğru Supabase projesine mi ait?
- `request_internal_auth_creation` function'ı var mı?
- `is_first_admin_setup_available` function'ı var mı?
- `profiles` tablosu var mı?
- Auth trigger yeni kullanıcı için profile oluşturuyor mu?
## Register Kapalı Görünüyor
`profiles` tablosunda herhangi bir kayıt varsa sistem ilk admin kurulmuş kabul eder.
Supabase SQL editor'da kontrol edebilirsiniz:
```sql
select id, role from public.profiles;
```
Geliştirme ortamında yanlış kayıt oluştuysa auth user ve profile kayıtlarını temizleyip tekrar deneyebilirsiniz.
## Veri Ekleniyor Ama Listede Görünmüyor
Bu çoğu zaman RLS kaynaklıdır.
Kontrol edin:
- Kayıtta `user_id` doğru mu?
- Policy `auth.uid() = user_id` mantığıyla uyumlu mu?
- Sorguda `.eq("user_id", user.id)` filtresi var mı?
- Kullanıcı session'ı gerçekten var mı?
Supabase bazen yetkisiz veriyi hata yerine boş sonuç olarak döndürür. Bu yüzden boş listeyi her zaman "veri yok" diye yorumlamamak gerekir.
## Storage Upload Hata Veriyor
Kontrol listesi:
- `avatars` bucket var mı?
- `project-assets` bucket var mı?
- `project-assets` private mı?
- Dosya boyutu 5 MB sınırını aşıyor mu?
- MIME type izinli mi?
- Storage policy service role erişimine izin veriyor mu?
- `SUPABASE_SERVICE_ROLE_KEY` doğru mu?
## Proje Görseli Yükleniyor Ama Görünmüyor
`project-assets` private bucket olduğu için public URL beklemeyin. Uygulama signed URL üretir.
Kontrol edin:
- `cover_image_path` tabloya yazılmış mı?
- Signed URL üretimi hata veriyor mu?
- Bucket adı doğru mu?
## AI Asistan Cevap Vermiyor
Kontrol edin:
- Ayarlar ekranında provider seçilmiş mi?
- API key kaydedilmiş mi?
- `app_settings` tablosunda kullanıcı için kayıt var mı?
- Provider ile model adı uyumlu mu?
- API key geçerli mi?
Chat endpoint'i için route:
```txt
/api/chat
```
## Finans veya Proje Analizi Hata Veriyor
Bu endpoint'ler de AI provider ayarlarını kullanır:
```txt
/api/finance-analysis
/api/project-risk
```
Eğer API key yoksa analiz endpoint'i hata döndürür.
## Build Hatası
Önce bağımlılıkları temiz kurmayı deneyin:
```bash
npm install
npm run build
```
Projede npm lockfile kullanılmalıdır. `pnpm-lock.yaml` veya başka lockfile'lar deployment platformunu karıştırabilir.
## Slow Filesystem Uyarısı
Next.js dev server bazen Windows veya yavaş disk üzerinde şu tarz uyarı verebilir:
```txt
Slow filesystem detected
```
Bu uyarı tek başına hata değildir. Ama dev server çok yavaşsa:
- Projeyi daha hızlı lokal diske taşıyın.
- `.next` klasörünü temizleyin.
- Antivirüs taramasının proje klasörünü yavaşlatmadığından emin olun.
## Health Check
Uygulamanın temel olarak ayağa kalktığını test etmek için:
```txt
/api/health
```
Beklenen cevap:
```json
{
"status": "ok"
}
```
Bu endpoint Supabase bağlantısını test etmez; sadece Next.js uygulamasının çalıştığını gösterir.
+149
View File
@@ -0,0 +1,149 @@
---
title: Güvenlik Notları
description: Neta'yı güvenli şekilde kullanmak için dikkat edilmesi gerekenler.
order: 16
---
# Güvenlik Notları
Neta kişisel kullanım ve self-host mantığıyla tasarlandı, ama yine de hassas veriler tutar. Müşteri bilgileri, proje detayları, finans kayıtları, AI API key'leri ve portal erişimleri doğru korunmalıdır.
Bu sayfada projeyi kullanırken özellikle dikkat ettiğim güvenlik noktalarını topladım.
## Service Role Key
`SUPABASE_SERVICE_ROLE_KEY` en hassas ortam değişkenidir. Bu key Supabase RLS kontrollerini bypass edebilir.
Bu yüzden:
- Asla browser'a gönderilmemelidir.
- Asla `NEXT_PUBLIC_` prefix'iyle tanımlanmamalıdır.
- Public repo içine yazılmamalıdır.
- `.env.local` commit edilmemelidir.
- Sadece deploy platformunun secret/env panelinde saklanmalıdır.
Bu key Neta'da sadece server tarafında kullanılır.
## Anon Key Güvenliği
`NEXT_PUBLIC_SUPABASE_ANON_KEY` public bir key'dir. Browser tarafında görünmesi normaldir.
Anon key'in güvenliği RLS policy'lerine bağlıdır. Yani anon key sızdı diye panik yapmak yerine, asıl kontrol edilmesi gereken şey Supabase policy'leridir.
Eğer RLS kapalıysa anon key ile veri erişimi tehlikeli hâle gelir.
## RLS Her Zaman Açık Olmalı
Neta'nın güvenlik modeli Supabase RLS üzerine kurulu.
Özellikle şu tablolarda RLS açık olmalıdır:
- `profiles`
- `clients`
- `projects`
- `tasks`
- `calendar_events`
- `finance_transactions`
- `daily_logs`
- `app_settings`
- `chat_sessions`
- `chat_messages`
- `project_planning_sections`
- `project_revisions`
- `client_activities`
Freelancer verileri genelde şu mantıkla korunur:
```sql
auth.uid() = user_id
```
## Client Portal İzolasyonu
Müşteri portalı açarken en kritik konu veri izolasyonudur.
Müşteri sadece kendi `client_auth_id` bağlantısına ait verileri görmelidir. Başka müşterilerin projelerini, finans kayıtlarını veya private görevleri görmemelidir.
Özellikle kontrol edilmesi gerekenler:
- `clients.client_auth_id`
- `projects.client_id`
- `tasks.is_public_to_client`
- `project_revisions.client_id`
Portal tarafında UI kontrolü olsa bile asıl güvenlik RLS policy içinde olmalıdır.
## AI API Key'leri
AI provider API key'leri `app_settings` tablosunda tutulur.
Bu pratik kişisel kullanım için sade bir çözüm sağlar, ama production güvenliği açısından şu noktalar önemlidir:
- `app_settings` tablosunda RLS açık olmalıdır.
- Kullanıcı sadece kendi ayarını okuyup yazabilmelidir.
- Service role key dışında toplu erişim olmamalıdır.
- Gerekiyorsa ileride API key encryption eklenmelidir.
## AI'ya Gönderilen Veri
AI asistan, kullanıcının görev, proje, finans ve günlük kayıtlarından bağlam üretir. Bu veri seçilen AI provider'a gönderilebilir.
Bu yüzden provider seçimi bilinçli yapılmalıdır.
Daha hassas kullanım için:
- Sadece gerekli alanları context'e ekleyin.
- Finans ve müşteri detaylarını sınırlayın.
- Local model/Ollama seçeneklerini değerlendirin.
- Prompt içinde kesin hukuki, finansal veya klinik hüküm verilmemesini belirtin.
## Storage Güvenliği
`avatars` bucket public olabilir. Ama `project-assets` private kalmalıdır.
Proje görselleri müşteri işleriyle ilgili olabileceği için herkese açık URL ile servis edilmemelidir. Signed URL yaklaşımı daha güvenlidir.
Storage policy'lerinde:
- Kullanıcı kendi klasörüne erişebilmeli.
- Service role server-side işlemler için erişebilmeli.
- Başka kullanıcıların klasörlerine erişim engellenmeli.
## Auth Ayarları
Supabase Auth tarafında production domain doğru tanımlanmalıdır.
Kontrol edilecek yer:
```txt
Authentication -> URL Configuration
```
Özellikle:
- Site URL doğru mu?
- Redirect URL listesi production domaini içeriyor mu?
- Geliştirme ve production domainleri karışmış mı?
## İlk Admin Modeli
Neta public çok kullanıcılı kayıt modeliyle çalışmaz. İlk admin oluşturulduktan sonra kayıt kapanır.
Bu davranış korunmalıdır. Eğer public register sürekli açık kalırsa başka kullanıcılar sisteme girebilir ve beklenmeyen veri/rol problemleri oluşabilir.
## Production Checklist
Production'a çıkmadan önce şunları kontrol ederim:
- `.env.local` commit edilmedi.
- `SUPABASE_SERVICE_ROLE_KEY` sadece server env içinde.
- RLS açık.
- Client portal policy'leri test edildi.
- Storage bucket policy'leri test edildi.
- Supabase Auth URL ayarları doğru.
- İlk admin oluşturuldu.
- Public register kilitleniyor.
- Müşteri sadece kendi portal verisini görüyor.
- AI provider bilinçli seçildi.
Bu kontroller tamamlandığında Neta kişisel kullanım için daha güvenli ve öngörülebilir hâle gelir.
-131
View File
@@ -1,131 +0,0 @@
# Self-Hosting Deployment
Neta has two Docker modes.
## Full-Stack Mode
Use this when the server should run Neta and the bundled Supabase-compatible stack.
Compose file:
```bash
docker-compose.full.yml
```
Services:
- `neta-web`: Next.js app
- `neta-db`: Postgres with pgvector
- `neta-auth`: Supabase Auth
- `neta-rest`: PostgREST
- `neta-storage`: Supabase Storage with local disk backend
- `neta-supabase-proxy`: single public API entrypoint
- `neta-migrations`: Neta database migration runner
Generate production secrets:
```bash
node scripts/generate-full-stack-env.mjs > .env
```
Then adjust these values before starting:
- `NEXT_PUBLIC_SITE_URL`: public Neta URL
- `NEXT_PUBLIC_SUPABASE_URL`: public bundled Supabase API URL
- `NETA_PORT`: host port for Neta when deploying directly with Docker
- `SUPABASE_API_PORT`: host port for the bundled Supabase API
- `POSTGRES_PORT`: host port for backup/manual DB access
Start:
```bash
docker compose -f docker-compose.full.yml up -d --build
```
Check the running stack:
```bash
sh ./scripts/selfhost-doctor.sh
```
To run a real Auth signup/login smoke test, enable the optional check:
```bash
NETA_DOCTOR_AUTH_SMOKE=1 sh ./scripts/selfhost-doctor.sh
```
## App-Only Mode
Use this when Neta connects to an existing Supabase or Supabase-compatible backend.
Compose file:
```bash
docker-compose.yml
```
Required env values:
- `NEXT_PUBLIC_SITE_URL`
- `NEXT_PUBLIC_SUPABASE_URL`
- `NEXT_PUBLIC_SUPABASE_ANON_KEY`
- `SUPABASE_SERVICE_ROLE_KEY`
- `DATABASE_URL` only when applying migrations manually
Apply migrations:
```bash
DATABASE_URL='postgresql://postgres:password@host:5432/postgres' sh ./scripts/apply-migrations.sh
```
Start:
```bash
docker compose up -d --build
```
## Coolify
For a no-external-service install, choose Docker Compose deployment and set the compose file to `docker-compose.full.yml`.
Set the generated full-stack env values in Coolify's environment variables. Route the app domain to `neta-web:3000`. If you expose Supabase through a second domain, route it to `neta-supabase-proxy:8000` and set `NEXT_PUBLIC_SUPABASE_URL` to that public URL.
## Dokploy
Create a Compose app from this repository. Use `docker-compose.dokploy.yml` for full-stack deployments and paste the generated env values into the environment panel. This compose file avoids fixed container names and host port bindings so Dokploy can route services itself.
Route the Neta domain to `neta-web` port `3000`. Route the bundled Supabase API domain, if used, to `neta-supabase-proxy` port `8000`.
## Backup And Restore
Full-stack mode stores the database in the `neta-db-data` Docker volume and uploaded files in the `neta-storage-data` Docker volume.
Create a backup:
```bash
sh ./scripts/selfhost-backup.sh
```
The backup is written to `./backups/<timestamp>/` and contains:
- `postgres.dump`: custom-format Postgres dump
- `storage.tar.gz`: local storage archive
- `manifest.txt`: backup metadata
Restore a backup:
```bash
sh ./scripts/selfhost-restore.sh ./backups/20260101T120000Z
```
Set `NETA_RESTORE_FORCE=1` for non-interactive restores:
```bash
NETA_RESTORE_FORCE=1 sh ./scripts/selfhost-restore.sh ./backups/20260101T120000Z
```
## First Admin
Open `/register` after the stack starts. The first registered user becomes the admin, and public registration is locked after that.
Full-stack deployments apply the first-admin registration guard automatically through `neta-migrations`; the installer should not run SQL manually.
-310
View File
@@ -1,310 +0,0 @@
#!/usr/bin/env bash
# Neta - Freelancer Operating System installer
set -euo pipefail
REPO_URL="${NETA_REPO_URL:-https://github.com/poyrazavsever/neta.git}"
TARGET_DIR="${NETA_TARGET_DIR:-neta-os}"
INSTALL_MODE="${NETA_INSTALL_MODE:-}"
APPLY_MIGRATIONS="${NETA_APPLY_MIGRATIONS:-}"
TTY_PATH="${NETA_TTY_PATH:-/dev/tty}"
info() {
printf "\n%s\n" "$1"
}
fail() {
echo "Error: $1" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || fail "$1 is required."
}
ensure_tty() {
if ! ( : < "$TTY_PATH" ) 2>/dev/null || ! ( : > "$TTY_PATH" ) 2>/dev/null; then
fail "Interactive input requires a TTY. Run with a terminal attached or provide the required NETA_* environment variables."
fi
}
read_from_tty() {
local label="$1"
local value
ensure_tty
printf "%s" "$label" > "$TTY_PATH"
IFS= read -r value < "$TTY_PATH" || fail "Input cancelled."
printf "%s" "$value"
}
read_secret_from_tty() {
local label="$1"
local value
ensure_tty
printf "%s" "$label" > "$TTY_PATH"
IFS= read -r -s value < "$TTY_PATH" || fail "Input cancelled."
printf "\n" > "$TTY_PATH"
printf "%s" "$value"
}
compose_cmd() {
if docker compose version >/dev/null 2>&1; then
echo "docker compose"
elif command -v docker-compose >/dev/null 2>&1; then
echo "docker-compose"
else
fail "Docker Compose is required."
fi
}
prompt_required() {
local var_name="$1"
local label="$2"
local current_value="${!var_name:-}"
local value
if [ -n "$current_value" ]; then
return
fi
while true; do
value="$(read_from_tty "$label: ")"
if [ -n "$value" ]; then
printf -v "$var_name" "%s" "$value"
export "$var_name"
return
fi
echo "This value is required."
done
}
prompt_optional() {
local var_name="$1"
local label="$2"
local default_value="$3"
local current_value="${!var_name:-}"
local value
if [ -n "$current_value" ]; then
return
fi
value="$(read_from_tty "$label [$default_value]: ")"
printf -v "$var_name" "%s" "${value:-$default_value}"
export "$var_name"
}
prompt_secret_required() {
local var_name="$1"
local label="$2"
local current_value="${!var_name:-}"
local value
if [ -n "$current_value" ]; then
return
fi
while true; do
value="$(read_secret_from_tty "$label: ")"
if [ -n "$value" ]; then
printf -v "$var_name" "%s" "$value"
export "$var_name"
return
fi
echo "This value is required."
done
}
choose_install_mode() {
if [ -n "$INSTALL_MODE" ]; then
case "$INSTALL_MODE" in
full|full-stack|bundled)
INSTALL_MODE="full-stack"
;;
app|app-only|external)
INSTALL_MODE="app-only"
;;
*)
fail "Invalid NETA_INSTALL_MODE. Use full-stack or app-only."
;;
esac
export INSTALL_MODE
return
fi
echo "Choose install mode:"
echo " 1) full-stack Neta + bundled Supabase/Postgres/Auth/Storage"
echo " 2) app-only Neta app connected to an existing Supabase project"
while true; do
answer="$(read_from_tty "Install mode [full-stack]: ")"
case "${answer:-full-stack}" in
1|full|full-stack|bundled)
INSTALL_MODE="full-stack"
export INSTALL_MODE
return
;;
2|app|app-only|external)
INSTALL_MODE="app-only"
export INSTALL_MODE
return
;;
*)
echo "Please choose full-stack or app-only."
;;
esac
done
}
run_node_script() {
local script="$1"
if command -v node >/dev/null 2>&1; then
node -e "$script"
else
docker run --rm \
-e ROLE="${ROLE:-}" \
-e JWT_SECRET="${JWT_SECRET:-}" \
node:22-alpine node -e "$script"
fi
}
random_secret() {
run_node_script "console.log(require('crypto').randomBytes(32).toString('hex'))"
}
generate_supabase_jwt() {
local role="$1"
local secret="$2"
ROLE="$role" JWT_SECRET="$secret" run_node_script "const crypto=require('crypto'); const b64=(v)=>Buffer.from(v).toString('base64url'); const header=b64(JSON.stringify({alg:'HS256',typ:'JWT'})); const payload=b64(JSON.stringify({iss:'supabase',ref:'neta',role:process.env.ROLE,iat:1700000000,exp:4102444800})); const unsigned=header+'.'+payload; const sig=crypto.createHmac('sha256', process.env.JWT_SECRET).update(unsigned).digest('base64url'); console.log(unsigned+'.'+sig);"
}
write_env_file() {
cat > .env <<EOF
NETA_INSTALL_MODE=$INSTALL_MODE
NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
NETA_PORT=$NETA_PORT
NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY
SUPABASE_SERVICE_ROLE_KEY=$SUPABASE_SERVICE_ROLE_KEY
EOF
if [ "$INSTALL_MODE" = "full-stack" ]; then
cat >> .env <<EOF
SUPABASE_API_PORT=$SUPABASE_API_PORT
POSTGRES_PORT=$POSTGRES_PORT
POSTGRES_PASSWORD=$POSTGRES_PASSWORD
JWT_SECRET=$JWT_SECRET
JWT_EXPIRY=${JWT_EXPIRY:-3600}
SMTP_ADMIN_EMAIL=${SMTP_ADMIN_EMAIL:-admin@neta.local}
EOF
fi
chmod 600 .env || true
}
configure_app_only() {
prompt_optional NEXT_PUBLIC_SITE_URL "Public Neta URL" "http://localhost:3000"
prompt_optional NETA_PORT "Host port for Neta" "3000"
prompt_required NEXT_PUBLIC_SUPABASE_URL "Supabase API URL"
prompt_secret_required NEXT_PUBLIC_SUPABASE_ANON_KEY "Supabase anon key"
prompt_secret_required SUPABASE_SERVICE_ROLE_KEY "Supabase service role key"
}
configure_full_stack() {
prompt_optional NEXT_PUBLIC_SITE_URL "Public Neta URL" "http://localhost:3000"
prompt_optional NETA_PORT "Host port for Neta" "3000"
prompt_optional NEXT_PUBLIC_SUPABASE_URL "Public Supabase API URL" "http://localhost:8000"
prompt_optional SUPABASE_API_PORT "Host port for bundled Supabase API" "8000"
prompt_optional POSTGRES_PORT "Host port for bundled Postgres" "54322"
POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-$(random_secret)}"
JWT_SECRET="${JWT_SECRET:-$(random_secret)}"
NEXT_PUBLIC_SUPABASE_ANON_KEY="${NEXT_PUBLIC_SUPABASE_ANON_KEY:-$(generate_supabase_jwt anon "$JWT_SECRET")}"
SUPABASE_SERVICE_ROLE_KEY="${SUPABASE_SERVICE_ROLE_KEY:-$(generate_supabase_jwt service_role "$JWT_SECRET")}"
export POSTGRES_PASSWORD JWT_SECRET NEXT_PUBLIC_SUPABASE_ANON_KEY SUPABASE_SERVICE_ROLE_KEY
}
run_compose() {
local compose="$1"
if [ "$INSTALL_MODE" = "full-stack" ]; then
info "Validating full-stack Docker Compose configuration"
$compose -f docker-compose.full.yml config >/dev/null
info "Building and starting bundled Neta stack"
$compose -f docker-compose.full.yml up -d --build
else
info "Validating Docker Compose configuration"
$compose config >/dev/null
info "Building and starting Neta"
$compose up -d --build
fi
}
main() {
info "Neta self-host installer"
require_command git
require_command docker
docker info >/dev/null 2>&1 || fail "Docker is installed but the daemon is not reachable."
if [ -d "$TARGET_DIR" ]; then
fail "'$TARGET_DIR' already exists. Set NETA_TARGET_DIR or remove the directory."
fi
info "Cloning Neta into $TARGET_DIR"
git clone "$REPO_URL" "$TARGET_DIR"
cd "$TARGET_DIR"
choose_install_mode
if [ "$INSTALL_MODE" = "full-stack" ]; then
configure_full_stack
else
configure_app_only
fi
write_env_file
info "Wrote .env"
if [ "$INSTALL_MODE" = "app-only" ]; then
local apply_migrations="$APPLY_MIGRATIONS"
if [ -z "$apply_migrations" ]; then
apply_migrations="$(read_from_tty "Apply Neta database migrations now? Requires a direct Postgres DATABASE_URL. [y/N]: ")"
fi
if [ "$apply_migrations" = "y" ] || [ "$apply_migrations" = "Y" ]; then
prompt_secret_required DATABASE_URL "Postgres DATABASE_URL"
DATABASE_URL="$DATABASE_URL" sh ./scripts/apply-migrations.sh
else
echo "Skipping migrations. Run them later with:"
echo " DATABASE_URL='postgresql://...' sh ./scripts/apply-migrations.sh"
fi
else
echo "Bundled mode applies migrations automatically through the neta-migrations service."
fi
local compose
compose="$(compose_cmd)"
run_compose "$compose"
info "Neta is starting"
echo "Open: $NEXT_PUBLIC_SITE_URL"
echo "Create the first admin account at: $NEXT_PUBLIC_SITE_URL/register"
if [ "$INSTALL_MODE" = "full-stack" ]; then
echo "Bundled Supabase API: $NEXT_PUBLIC_SUPABASE_URL"
echo "Bundled Postgres host port: $POSTGRES_PORT"
fi
}
main "$@"
-63
View File
@@ -1,63 +0,0 @@
@echo off
setlocal
cd /d "%~dp0"
echo [1/4] Python kontrol ediliyor...
set "PY_CMD="
where python >nul 2>nul
if %errorlevel%==0 (
python -c "import sys; raise SystemExit(0 if sys.version_info.major==3 else 1)" >nul 2>nul
if %errorlevel%==0 set "PY_CMD=python"
)
if not defined PY_CMD (
where py >nul 2>nul
if %errorlevel%==0 (
py -3.13 -c "import sys" >nul 2>nul
if %errorlevel%==0 (
set "PY_CMD=py -3.13"
) else (
py -3 -c "import sys" >nul 2>nul
if %errorlevel%==0 (
set "PY_CMD=py -3"
) else (
py -c "import sys" >nul 2>nul
if %errorlevel%==0 set "PY_CMD=py"
)
)
)
)
if not defined PY_CMD (
echo [HATA] Uygun Python 3 bulunamadi.
echo [HATA] Python 3.13 kurup tekrar deneyin: https://www.python.org/downloads/
exit /b 1
)
echo [2/4] Sanal ortam olusturuluyor...
if not exist ".venv\Scripts\python.exe" (
%PY_CMD% -m venv .venv
if not %errorlevel%==0 (
echo [HATA] .venv olusturulamadi.
exit /b 1
)
)
echo [3/4] pip guncelleniyor...
call ".venv\Scripts\activate.bat"
python -m pip install --upgrade pip
if not %errorlevel%==0 (
echo [HATA] pip guncellenemedi.
exit /b 1
)
echo [4/4] Paketler kuruluyor...
pip install -r requirements.txt
if not %errorlevel%==0 (
echo [HATA] Paket kurulumu basarisiz.
exit /b 1
)
echo [OK] Kurulum tamamlandi.
exit /b 0
+29 -6
View File
@@ -5,6 +5,22 @@ type FirstAdminSetupState = {
errorMessage?: string; errorMessage?: string;
}; };
type FirstAdminSetupOptions = {
timeoutMs?: number;
};
const DEFAULT_SETUP_TIMEOUT_MS = 5_000;
function createTimeoutSignal(timeoutMs: number) {
if (typeof AbortSignal !== "undefined" && "timeout" in AbortSignal) {
return AbortSignal.timeout(timeoutMs);
}
const controller = new AbortController();
setTimeout(() => controller.abort(), timeoutMs).unref?.();
return controller.signal;
}
function isMissingSetupFunctionError(error: { function isMissingSetupFunctionError(error: {
code?: string; code?: string;
message?: string; message?: string;
@@ -15,9 +31,10 @@ function isMissingSetupFunctionError(error: {
); );
} }
async function getSetupStateFromProfiles(): Promise<FirstAdminSetupState | null> { async function getSetupStateFromProfiles(
const supabaseUrl = timeoutMs: number,
process.env.SUPABASE_INTERNAL_URL || process.env.NEXT_PUBLIC_SUPABASE_URL; ): Promise<FirstAdminSetupState | null> {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY; const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (!supabaseUrl || !serviceRoleKey) { if (!supabaseUrl || !serviceRoleKey) {
@@ -31,6 +48,7 @@ async function getSetupStateFromProfiles(): Promise<FirstAdminSetupState | null>
try { try {
const response = await fetch(endpoint, { const response = await fetch(endpoint, {
cache: "no-store", cache: "no-store",
signal: createTimeoutSignal(timeoutMs),
headers: { headers: {
apikey: serviceRoleKey, apikey: serviceRoleKey,
authorization: `Bearer ${serviceRoleKey}`, authorization: `Bearer ${serviceRoleKey}`,
@@ -53,9 +71,14 @@ async function getSetupStateFromProfiles(): Promise<FirstAdminSetupState | null>
} }
} }
export async function getFirstAdminSetupState(): Promise<FirstAdminSetupState> { export async function getFirstAdminSetupState(
options: FirstAdminSetupOptions = {},
): Promise<FirstAdminSetupState> {
const timeoutMs = options.timeoutMs ?? DEFAULT_SETUP_TIMEOUT_MS;
const supabase = await createClient(); const supabase = await createClient();
const { data, error } = await supabase.rpc("is_first_admin_setup_available"); const { data, error } = await supabase
.rpc("is_first_admin_setup_available")
.abortSignal(createTimeoutSignal(timeoutMs));
if (error) { if (error) {
console.error("First admin setup check failed", { console.error("First admin setup check failed", {
@@ -65,7 +88,7 @@ export async function getFirstAdminSetupState(): Promise<FirstAdminSetupState> {
hint: error.hint, hint: error.hint,
}); });
const fallbackState = await getSetupStateFromProfiles(); const fallbackState = await getSetupStateFromProfiles(timeoutMs);
if (fallbackState) { if (fallbackState) {
if (isMissingSetupFunctionError(error)) { if (isMissingSetupFunctionError(error)) {
+1 -2
View File
@@ -1,8 +1,7 @@
import { createClient } from "@supabase/supabase-js"; import { createClient } from "@supabase/supabase-js";
export function createServiceRoleClient() { export function createServiceRoleClient() {
const supabaseUrl = const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
process.env.SUPABASE_INTERNAL_URL || process.env.NEXT_PUBLIC_SUPABASE_URL;
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY; const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (!supabaseUrl || !serviceRoleKey) { if (!supabaseUrl || !serviceRoleKey) {
+15 -6
View File
@@ -2,14 +2,23 @@ import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server' import { NextResponse, type NextRequest } from 'next/server'
export async function updateSession(request: NextRequest) { export async function updateSession(request: NextRequest) {
const pathname = request.nextUrl.pathname
if (
pathname.startsWith('/login') ||
pathname.startsWith('/register') ||
pathname.startsWith('/auth') ||
pathname.startsWith('/forgot-password')
) {
return NextResponse.next({ request })
}
let supabaseResponse = NextResponse.next({ let supabaseResponse = NextResponse.next({
request, request,
}) })
const supabaseUrl =
process.env.SUPABASE_INTERNAL_URL || process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabase = createServerClient( const supabase = createServerClient(
supabaseUrl, process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ {
cookies: { cookies: {
@@ -39,9 +48,9 @@ export async function updateSession(request: NextRequest) {
if ( if (
!user && !user &&
!request.nextUrl.pathname.startsWith('/login') && !pathname.startsWith('/login') &&
!request.nextUrl.pathname.startsWith('/register') && !pathname.startsWith('/register') &&
!request.nextUrl.pathname.startsWith('/auth') !pathname.startsWith('/auth')
) { ) {
// no user, potentially respond by redirecting the user to the login page // no user, potentially respond by redirecting the user to the login page
const url = request.nextUrl.clone() const url = request.nextUrl.clone()
+1 -3
View File
@@ -3,11 +3,9 @@ import { cookies } from 'next/headers'
export async function createClient() { export async function createClient() {
const cookieStore = await cookies() const cookieStore = await cookies()
const supabaseUrl =
process.env.SUPABASE_INTERNAL_URL || process.env.NEXT_PUBLIC_SUPABASE_URL!
return createServerClient( return createServerClient(
supabaseUrl, process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ {
cookies: { cookies: {
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts"; import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
-1
View File
@@ -8,7 +8,6 @@ const withPWA = withPWAInit({
}); });
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: "standalone",
experimental: { experimental: {
serverActions: { serverActions: {
bodySizeLimit: "8mb", bodySizeLimit: "8mb",
+184 -146
View File
@@ -1,11 +1,11 @@
{ {
"name": "mood-tracker-mvp", "name": "neta",
"version": "0.1.0", "version": "0.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mood-tracker-mvp", "name": "neta",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@ai-sdk/google": "^3.0.80", "@ai-sdk/google": "^3.0.80",
@@ -29,6 +29,7 @@
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-toast": "^1.2.15", "@radix-ui/react-toast": "^1.2.15",
"@supabase/ssr": "^0.10.3", "@supabase/ssr": "^0.10.3",
"@supabase/supabase-js": "^2.105.3",
"@tailwindcss/postcss": "^4.3.0", "@tailwindcss/postcss": "^4.3.0",
"ai": "^6.0.197", "ai": "^6.0.197",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@@ -45,7 +46,7 @@
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^19.2.7", "react-dom": "^19.2.7",
"react-hook-form": "^7.77.0", "react-hook-form": "^7.77.0",
"recharts": "^2.15.4", "recharts": "^3.8.1",
"shadcn": "^4.10.0", "shadcn": "^4.10.0",
"tailwind-merge": "^3.6.0", "tailwind-merge": "^3.6.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
@@ -217,6 +218,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.29.0",
@@ -1863,6 +1865,7 @@
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
@@ -2046,6 +2049,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -2385,6 +2389,7 @@
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.4.0.tgz", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.4.0.tgz",
"integrity": "sha512-EIsqr/t/qbinPIhGjMdtvutIN1Kk4uwbROE9/UQ93CAVGR7GkA7Y92+fX80OzXi/OB67jVFYwKGO1WzkxmkFZw==", "integrity": "sha512-EIsqr/t/qbinPIhGjMdtvutIN1Kk4uwbROE9/UQ93CAVGR7GkA7Y92+fX80OzXi/OB67jVFYwKGO1WzkxmkFZw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@standard-schema/utils": "^0.3.0" "@standard-schema/utils": "^0.3.0"
}, },
@@ -3223,6 +3228,7 @@
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": "^14.21.3 || >=16" "node": "^14.21.3 || >=16"
}, },
@@ -5045,6 +5051,42 @@
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@reduxjs/toolkit": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz",
"integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@standard-schema/utils": "^0.3.0",
"immer": "^11.0.0",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"reselect": "^5.1.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-redux": {
"optional": true
}
}
},
"node_modules/@reduxjs/toolkit/node_modules/immer": {
"version": "11.1.8",
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz",
"integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/@rollup/plugin-babel": { "node_modules/@rollup/plugin-babel": {
"version": "5.3.1", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -5262,7 +5304,6 @@
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.105.3.tgz", "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.105.3.tgz",
"integrity": "sha512-hMFuzP++mjRfe0/BUq4/e82CXIDgyjUgg0khLN8waol/gzoM1t2iGmhfJSGvQHQ1dr3XqWpP6ThAw4bLHMot5Q==", "integrity": "sha512-hMFuzP++mjRfe0/BUq4/e82CXIDgyjUgg0khLN8waol/gzoM1t2iGmhfJSGvQHQ1dr3XqWpP6ThAw4bLHMot5Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "2.8.1" "tslib": "2.8.1"
}, },
@@ -5275,7 +5316,6 @@
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.105.3.tgz", "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.105.3.tgz",
"integrity": "sha512-KyutUwLLUZ9fRXsiFACL6lq7akBVHFl0fnqQnrxjbsPco8jeb4EyirQuvr52QCLnikzjMRC0uxAHOSM54aDrZA==", "integrity": "sha512-KyutUwLLUZ9fRXsiFACL6lq7akBVHFl0fnqQnrxjbsPco8jeb4EyirQuvr52QCLnikzjMRC0uxAHOSM54aDrZA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "2.8.1" "tslib": "2.8.1"
}, },
@@ -5287,15 +5327,13 @@
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.1.tgz", "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.1.tgz",
"integrity": "sha512-hWGJkDAfWUNY8k0C080u3sGNFd2ncl9erhKgP7hnGkgJWEfT5Pd/SXal4QmWXBECVlZrannMAc9sBaaRyWpiUA==", "integrity": "sha512-hWGJkDAfWUNY8k0C080u3sGNFd2ncl9erhKgP7hnGkgJWEfT5Pd/SXal4QmWXBECVlZrannMAc9sBaaRyWpiUA==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/@supabase/postgrest-js": { "node_modules/@supabase/postgrest-js": {
"version": "2.105.3", "version": "2.105.3",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.105.3.tgz", "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.105.3.tgz",
"integrity": "sha512-jFVYRHcri0ZMcTzKpQ2r2wWOB8/rPsbj92kxmCmVJUiRrdgiMtuYlkS06Fhs8UJZhEOL0UpGhh06XDwh8JwtBQ==", "integrity": "sha512-jFVYRHcri0ZMcTzKpQ2r2wWOB8/rPsbj92kxmCmVJUiRrdgiMtuYlkS06Fhs8UJZhEOL0UpGhh06XDwh8JwtBQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "2.8.1" "tslib": "2.8.1"
}, },
@@ -5308,7 +5346,6 @@
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.105.3.tgz", "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.105.3.tgz",
"integrity": "sha512-L+qPiJlq1RKh3QD2fORGCFo2RKDKlvG9mjvPtUEQJ2tMixrx70VIV6j8BdWzQkbc1Nao6mvTWajyDhX3TFgljw==", "integrity": "sha512-L+qPiJlq1RKh3QD2fORGCFo2RKDKlvG9mjvPtUEQJ2tMixrx70VIV6j8BdWzQkbc1Nao6mvTWajyDhX3TFgljw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@supabase/phoenix": "^0.4.1", "@supabase/phoenix": "^0.4.1",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
@@ -5349,7 +5386,6 @@
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.105.3.tgz", "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.105.3.tgz",
"integrity": "sha512-M7oPCCcHim/FsR6rKIs10Nd9mW051N2SQvA27jiVLa7oQMFFb7faX5dCQRV4GS5QeFsBcV5J/fWl4Ppoaw8cBQ==", "integrity": "sha512-M7oPCCcHim/FsR6rKIs10Nd9mW051N2SQvA27jiVLa7oQMFFb7faX5dCQRV4GS5QeFsBcV5J/fWl4Ppoaw8cBQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"iceberg-js": "^0.8.1", "iceberg-js": "^0.8.1",
"tslib": "2.8.1" "tslib": "2.8.1"
@@ -5855,6 +5891,7 @@
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@@ -5865,6 +5902,7 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
@@ -5881,6 +5919,12 @@
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
"node_modules/@types/uuid": { "node_modules/@types/uuid": {
"version": "10.0.0", "version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
@@ -5899,7 +5943,6 @@
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
} }
@@ -5949,6 +5992,7 @@
"integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/scope-manager": "8.60.1",
"@typescript-eslint/types": "8.60.1", "@typescript-eslint/types": "8.60.1",
@@ -6469,7 +6513,6 @@
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
"integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-numbers": "1.13.2",
"@webassemblyjs/helper-wasm-bytecode": "1.13.2" "@webassemblyjs/helper-wasm-bytecode": "1.13.2"
@@ -6479,29 +6522,25 @@
"version": "1.13.2", "version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
"integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/@webassemblyjs/helper-api-error": { "node_modules/@webassemblyjs/helper-api-error": {
"version": "1.13.2", "version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
"integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/@webassemblyjs/helper-buffer": { "node_modules/@webassemblyjs/helper-buffer": {
"version": "1.14.1", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
"integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/@webassemblyjs/helper-numbers": { "node_modules/@webassemblyjs/helper-numbers": {
"version": "1.13.2", "version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
"integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/floating-point-hex-parser": "1.13.2",
"@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2",
@@ -6512,15 +6551,13 @@
"version": "1.13.2", "version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
"integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/@webassemblyjs/helper-wasm-section": { "node_modules/@webassemblyjs/helper-wasm-section": {
"version": "1.14.1", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
"integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@webassemblyjs/ast": "1.14.1", "@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1",
@@ -6533,7 +6570,6 @@
"resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
"integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@xtuc/ieee754": "^1.2.0" "@xtuc/ieee754": "^1.2.0"
} }
@@ -6543,7 +6579,6 @@
"resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
"integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@xtuc/long": "4.2.2" "@xtuc/long": "4.2.2"
} }
@@ -6552,15 +6587,13 @@
"version": "1.13.2", "version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
"integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/@webassemblyjs/wasm-edit": { "node_modules/@webassemblyjs/wasm-edit": {
"version": "1.14.1", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
"integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@webassemblyjs/ast": "1.14.1", "@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1",
@@ -6577,7 +6610,6 @@
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
"integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@webassemblyjs/ast": "1.14.1", "@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
@@ -6591,7 +6623,6 @@
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
"integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@webassemblyjs/ast": "1.14.1", "@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1",
@@ -6604,7 +6635,6 @@
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
"integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@webassemblyjs/ast": "1.14.1", "@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2",
@@ -6619,7 +6649,6 @@
"resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
"integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@webassemblyjs/ast": "1.14.1", "@webassemblyjs/ast": "1.14.1",
"@xtuc/long": "4.2.2" "@xtuc/long": "4.2.2"
@@ -6629,15 +6658,13 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
"integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause"
"peer": true
}, },
"node_modules/@xtuc/long": { "node_modules/@xtuc/long": {
"version": "4.2.2", "version": "4.2.2",
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"license": "Apache-2.0", "license": "Apache-2.0"
"peer": true
}, },
"node_modules/accepts": { "node_modules/accepts": {
"version": "2.0.0", "version": "2.0.0",
@@ -6657,6 +6684,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -6669,7 +6697,6 @@
"resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=10.13.0" "node": ">=10.13.0"
}, },
@@ -7230,6 +7257,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.10.12", "baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782", "caniuse-lite": "^1.0.30001782",
@@ -7372,7 +7400,6 @@
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
"integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=6.0" "node": ">=6.0"
} }
@@ -7619,6 +7646,7 @@
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/d3-array": { "node_modules/d3-array": {
@@ -7814,6 +7842,7 @@
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz",
"integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/kossnocorp" "url": "https://github.com/sponsors/kossnocorp"
@@ -7983,7 +8012,8 @@
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/dexie/-/dexie-4.4.3.tgz", "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.4.3.tgz",
"integrity": "sha512-N+3IGQ3HPlyO2YAkntGAwitm42BpBGV86MttzUMiRzWLa4NGh0pltVRcUVF4ybL/OnXjCrr9k7SDPIKkFYP2Lg==", "integrity": "sha512-N+3IGQ3HPlyO2YAkntGAwitm42BpBGV86MttzUMiRzWLa4NGh0pltVRcUVF4ybL/OnXjCrr9k7SDPIKkFYP2Lg==",
"license": "Apache-2.0" "license": "Apache-2.0",
"peer": true
}, },
"node_modules/dexie-react-hooks": { "node_modules/dexie-react-hooks": {
"version": "4.4.0", "version": "4.4.0",
@@ -8017,16 +8047,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "17.4.2", "version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
@@ -8262,8 +8282,7 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
"integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/es-object-atoms": { "node_modules/es-object-atoms": {
"version": "1.1.1", "version": "1.1.1",
@@ -8322,6 +8341,16 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/es-toolkit": {
"version": "1.47.1",
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.1.tgz",
"integrity": "sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q==",
"license": "MIT",
"workspaces": [
"docs",
"benchmarks"
]
},
"node_modules/escalade": { "node_modules/escalade": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -8356,6 +8385,7 @@
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -8541,6 +8571,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@rtsao/scc": "^1.1.0", "@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9", "array-includes": "^3.1.9",
@@ -8802,9 +8833,9 @@
} }
}, },
"node_modules/eventemitter3": { "node_modules/eventemitter3": {
"version": "4.0.7", "version": "5.0.4",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/events": { "node_modules/events": {
@@ -8812,7 +8843,6 @@
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.8.x" "node": ">=0.8.x"
} }
@@ -8869,6 +8899,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"accepts": "^2.0.0", "accepts": "^2.0.0",
"body-parser": "^2.2.1", "body-parser": "^2.2.1",
@@ -8931,15 +8962,6 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-equals": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
"integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/fast-glob": { "node_modules/fast-glob": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
@@ -9502,8 +9524,7 @@
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause"
"peer": true
}, },
"node_modules/globals": { "node_modules/globals": {
"version": "14.0.0", "version": "14.0.0",
@@ -9661,6 +9682,7 @@
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.17.tgz", "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.17.tgz",
"integrity": "sha512-FbJJNb/XgX7YW0hX/V8w5oYLztKEsRLykCMZWt1WdLtsfjzMvmoqWBA4H4t5norinq8/rh20oiZYr+WSl4UzAQ==", "integrity": "sha512-FbJJNb/XgX7YW0hX/V8w5oYLztKEsRLykCMZWt1WdLtsfjzMvmoqWBA4H4t5norinq8/rh20oiZYr+WSl4UzAQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=16.9.0" "node": ">=16.9.0"
} }
@@ -9712,7 +9734,6 @@
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"
} }
@@ -9748,6 +9769,16 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/immer": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -10411,7 +10442,6 @@
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
"integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/node": "*", "@types/node": "*",
"merge-stream": "^2.0.0", "merge-stream": "^2.0.0",
@@ -10426,7 +10456,6 @@
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"has-flag": "^4.0.0" "has-flag": "^4.0.0"
}, },
@@ -10896,7 +10925,6 @@
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz",
"integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=6.11.5" "node": ">=6.11.5"
}, },
@@ -10990,6 +11018,7 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0" "js-tokens": "^3.0.0 || ^4.0.0"
@@ -11225,14 +11254,14 @@
"version": "2.6.2", "version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/next": { "node_modules/next": {
"version": "16.2.7", "version": "16.2.7",
"resolved": "https://registry.npmjs.org/next/-/next-16.2.7.tgz", "resolved": "https://registry.npmjs.org/next/-/next-16.2.7.tgz",
"integrity": "sha512-eMJxgjRzBaj3olkP4cBamHDXL79A8FC6u1GcsO1D1Tsx8bw/LLXUJCaoajVxtnhD3A1IJqIT8IcRJjgBIPJq4w==", "integrity": "sha512-eMJxgjRzBaj3olkP4cBamHDXL79A8FC6u1GcsO1D1Tsx8bw/LLXUJCaoajVxtnhD3A1IJqIT8IcRJjgBIPJq4w==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@next/env": "16.2.7", "@next/env": "16.2.7",
"@swc/helpers": "0.5.15", "@swc/helpers": "0.5.15",
@@ -11852,6 +11881,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.12", "nanoid": "^3.3.12",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -12097,6 +12127,7 @@
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"loose-envify": "^1.4.0", "loose-envify": "^1.4.0",
@@ -12340,6 +12371,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -12349,6 +12381,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@@ -12361,6 +12394,7 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.78.0.tgz", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.78.0.tgz",
"integrity": "sha512-EEZqc+N23moyzTlz61Pj+JvcXo76ICkpfOZo8JZw+sM4+wLQGh6nI2Ms+PdMOYNluFu0ghlM7B8mCzhRYtJCnA==", "integrity": "sha512-EEZqc+N23moyzTlz61Pj+JvcXo76ICkpfOZo8JZw+sM4+wLQGh6nI2Ms+PdMOYNluFu0ghlM7B8mCzhRYtJCnA==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@@ -12376,7 +12410,32 @@
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT" "license": "MIT",
"peer": true
},
"node_modules/react-redux": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz",
"integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"@types/react": "^18.2.25 || ^19",
"react": "^18.0 || ^19",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"redux": {
"optional": true
}
}
}, },
"node_modules/react-remove-scroll": { "node_modules/react-remove-scroll": {
"version": "2.7.2", "version": "2.7.2",
@@ -12425,21 +12484,6 @@
} }
} }
}, },
"node_modules/react-smooth": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
"integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
"license": "MIT",
"dependencies": {
"fast-equals": "^5.0.1",
"prop-types": "^15.8.1",
"react-transition-group": "^4.4.5"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-style-singleton": { "node_modules/react-style-singleton": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
@@ -12462,22 +12506,6 @@
} }
} }
}, },
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"license": "BSD-3-Clause",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/recast": { "node_modules/recast": {
"version": "0.23.11", "version": "0.23.11",
"resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz",
@@ -12495,43 +12523,57 @@
} }
}, },
"node_modules/recharts": { "node_modules/recharts": {
"version": "2.15.4", "version": "3.8.1",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz",
"integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==",
"license": "MIT", "license": "MIT",
"workspaces": [
"www"
],
"dependencies": { "dependencies": {
"clsx": "^2.0.0", "@reduxjs/toolkit": "^1.9.0 || 2.x.x",
"eventemitter3": "^4.0.1", "clsx": "^2.1.1",
"lodash": "^4.17.21", "decimal.js-light": "^2.5.1",
"react-is": "^18.3.1", "es-toolkit": "^1.39.3",
"react-smooth": "^4.0.4", "eventemitter3": "^5.0.1",
"recharts-scale": "^0.4.4", "immer": "^10.1.1",
"tiny-invariant": "^1.3.1", "react-redux": "8.x.x || 9.x.x",
"victory-vendor": "^36.6.8" "reselect": "5.1.1",
"tiny-invariant": "^1.3.3",
"use-sync-external-store": "^1.2.2",
"victory-vendor": "^37.0.2"
}, },
"engines": { "engines": {
"node": ">=14" "node": ">=18"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
} }
}, },
"node_modules/recharts-scale": { "node_modules/recharts/node_modules/reselect": {
"version": "0.4.5", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
"license": "MIT",
"dependencies": {
"decimal.js-light": "^2.4.1"
}
},
"node_modules/recharts/node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT",
"peer": true
},
"node_modules/redux-thunk": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
"license": "MIT",
"peerDependencies": {
"redux": "^5.0.0"
}
},
"node_modules/reflect.getprototypeof": { "node_modules/reflect.getprototypeof": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -12716,6 +12758,7 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz",
"integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"rollup": "dist/bin/rollup" "rollup": "dist/bin/rollup"
}, },
@@ -12876,7 +12919,6 @@
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
"integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/json-schema": "^7.0.9", "@types/json-schema": "^7.0.9",
"ajv": "^8.9.0", "ajv": "^8.9.0",
@@ -12913,7 +12955,6 @@
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"ajv": "^8.0.0" "ajv": "^8.0.0"
}, },
@@ -12931,7 +12972,6 @@
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3" "fast-deep-equal": "^3.1.3"
}, },
@@ -12943,8 +12983,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.4", "version": "7.7.4",
@@ -13733,7 +13772,8 @@
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz",
"integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/tailwindcss-animate": { "node_modules/tailwindcss-animate": {
"version": "1.0.7", "version": "1.0.7",
@@ -13819,7 +13859,6 @@
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz",
"integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@jridgewell/trace-mapping": "^0.3.25", "@jridgewell/trace-mapping": "^0.3.25",
"jest-worker": "^27.4.5", "jest-worker": "^27.4.5",
@@ -13934,6 +13973,7 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -14132,6 +14172,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -14458,9 +14499,9 @@
} }
}, },
"node_modules/victory-vendor": { "node_modules/victory-vendor": {
"version": "36.9.2", "version": "37.3.6",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
"integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
"license": "MIT AND ISC", "license": "MIT AND ISC",
"dependencies": { "dependencies": {
"@types/d3-array": "^3.0.3", "@types/d3-array": "^3.0.3",
@@ -14484,7 +14525,6 @@
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz",
"integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"glob-to-regexp": "^0.4.1", "glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.1.2" "graceful-fs": "^4.1.2"
@@ -14513,7 +14553,6 @@
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.2.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.2.tgz",
"integrity": "sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==", "integrity": "sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/estree": "^1.0.8", "@types/estree": "^1.0.8",
"@types/json-schema": "^7.0.15", "@types/json-schema": "^7.0.15",
@@ -14560,7 +14599,6 @@
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz",
"integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=10.13.0" "node": ">=10.13.0"
} }
@@ -14570,7 +14608,6 @@
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"peer": true,
"dependencies": { "dependencies": {
"esrecurse": "^4.3.0", "esrecurse": "^4.3.0",
"estraverse": "^4.1.1" "estraverse": "^4.1.1"
@@ -14584,7 +14621,6 @@
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"peer": true,
"engines": { "engines": {
"node": ">=4.0" "node": ">=4.0"
} }
@@ -14798,6 +14834,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1", "fast-uri": "^3.0.1",
@@ -15029,6 +15066,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1", "fast-uri": "^3.0.1",
@@ -15185,7 +15223,6 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
}, },
@@ -15269,6 +15306,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
+2 -6
View File
@@ -1,16 +1,12 @@
{ {
"name": "mood-tracker-mvp", "name": "neta",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "eslint .", "lint": "eslint ."
"selfhost:env": "node scripts/generate-full-stack-env.mjs",
"selfhost:doctor": "sh scripts/selfhost-doctor.sh",
"selfhost:backup": "sh scripts/selfhost-backup.sh",
"selfhost:restore": "sh scripts/selfhost-restore.sh"
}, },
"dependencies": { "dependencies": {
"@ai-sdk/google": "^3.0.80", "@ai-sdk/google": "^3.0.80",
-11090
View File
File diff suppressed because it is too large Load Diff
-7
View File
@@ -1,7 +0,0 @@
nodeLinker: hoisted
allowBuilds:
msw: false
sharp: false
unrs-resolver: false
minimumReleaseAgeExclude:
- poyraz-ui@2.1.0
-176
View File
@@ -1,176 +0,0 @@
#!/usr/bin/env sh
set -eu
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
PSQL_IMAGE="${NETA_PSQL_IMAGE:-postgres:16-alpine}"
if [ -z "${DATABASE_URL:-}" ]; then
echo "DATABASE_URL is required." >&2
echo "Example:" >&2
echo " DATABASE_URL='postgresql://postgres:password@host:5432/postgres' sh ./scripts/apply-migrations.sh" >&2
exit 1
fi
run_sql() {
sql="$1"
if command -v psql >/dev/null 2>&1; then
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -Atc "$sql"
elif command -v docker >/dev/null 2>&1; then
docker run --rm -i "$PSQL_IMAGE" \
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -Atc "$sql"
else
echo "Neither psql nor docker is available to run migrations." >&2
exit 1
fi
}
query_scalar() {
run_sql "$1" | tail -n 1 | tr -d '[:space:]'
}
ensure_migration_table() {
run_sql "
create schema if not exists neta_internal;
create table if not exists neta_internal.schema_migrations (
version text primary key,
file_path text not null,
applied_at timestamp with time zone default timezone('utc'::text, now()) not null
);
revoke all on schema neta_internal from public;
revoke all on all tables in schema neta_internal from public;
" >/dev/null
}
is_migration_recorded() {
migration_id="$1"
result="$(query_scalar "select case when exists (select 1 from neta_internal.schema_migrations where version = '$migration_id') then 'yes' else 'no' end;")"
[ "$result" = "yes" ]
}
record_migration() {
migration_id="$1"
file_path="$2"
run_sql "
insert into neta_internal.schema_migrations (version, file_path)
values ('$migration_id', '$file_path')
on conflict (version) do update set
file_path = excluded.file_path,
applied_at = timezone('utc'::text, now());
" >/dev/null
}
existing_objects_cover_migration() {
migration_id="$1"
case "$migration_id" in
0001_schema)
query_scalar "select case when to_regclass('public.profiles') is not null and to_regclass('public.tasks') is not null then 'yes' else 'no' end;"
;;
0002_freelancer_os_core)
query_scalar "select case when to_regclass('public.clients') is not null and to_regclass('public.projects') is not null and to_regclass('public.calendar_events') is not null and to_regclass('public.finance_transactions') is not null and to_regclass('public.daily_logs') is not null and exists (select 1 from information_schema.columns where table_schema = 'public' and table_name = 'tasks' and column_name = 'priority') then 'yes' else 'no' end;"
;;
0003_project_planning_assets)
query_scalar "select case when to_regclass('public.project_planning_sections') is not null and exists (select 1 from information_schema.columns where table_schema = 'public' and table_name = 'projects' and column_name = 'cover_image_path') then 'yes' else 'no' end;"
;;
0004_business_os_tables)
query_scalar "select case when to_regclass('public.proposals') is not null and to_regclass('public.contracts') is not null and to_regclass('public.invoices') is not null and to_regclass('public.subscriptions') is not null then 'yes' else 'no' end;"
;;
0005_advanced_crm)
query_scalar "select case when to_regclass('public.client_activities') is not null and exists (select 1 from information_schema.columns where table_schema = 'public' and table_name = 'clients' and column_name = 'pipeline_stage') then 'yes' else 'no' end;"
;;
0006_pgvector_embeddings)
query_scalar "select case when to_regclass('public.document_embeddings') is not null and exists (select 1 from pg_proc p join pg_namespace n on n.oid = p.pronamespace where n.nspname = 'public' and p.proname = 'match_documents') then 'yes' else 'no' end;"
;;
0007_client_portal)
query_scalar "select case when to_regclass('public.project_revisions') is not null and exists (select 1 from information_schema.columns where table_schema = 'public' and table_name = 'profiles' and column_name = 'role') and exists (select 1 from information_schema.columns where table_schema = 'public' and table_name = 'clients' and column_name = 'client_auth_id') then 'yes' else 'no' end;"
;;
0008_project_progress_quota)
query_scalar "select case when exists (select 1 from information_schema.columns where table_schema = 'public' and table_name = 'projects' and column_name = 'progress_type') and exists (select 1 from pg_proc p join pg_namespace n on n.oid = p.pronamespace where n.nspname = 'public' and p.proname = 'update_project_progress_on_task_change') then 'yes' else 'no' end;"
;;
0009_first_admin_registration_lock)
echo "no"
;;
0010_internal_auth_creation)
echo "no"
;;
0011_service_role_claims_storage)
echo "no"
;;
*)
echo "no"
;;
esac
}
run_sql_file() {
file_path="$1"
absolute_path="$ROOT_DIR/$file_path"
if [ ! -f "$absolute_path" ]; then
echo "Missing SQL file: $file_path" >&2
exit 1
fi
echo "Applying $file_path"
if command -v psql >/dev/null 2>&1; then
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f "$absolute_path"
elif command -v docker >/dev/null 2>&1; then
docker run --rm -i "$PSQL_IMAGE" \
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f - < "$absolute_path"
else
echo "Neither psql nor docker is available to run migrations." >&2
exit 1
fi
}
run_migration() {
migration_id="$1"
file_path="$2"
if is_migration_recorded "$migration_id"; then
echo "Skipping $file_path; migration $migration_id is already recorded."
return
fi
if [ "$(existing_objects_cover_migration "$migration_id")" = "yes" ]; then
echo "Detected existing database objects for $migration_id; recording without replay."
record_migration "$migration_id" "$file_path"
return
fi
run_sql_file "$file_path"
record_migration "$migration_id" "$file_path"
}
reload_postgrest_schema_cache() {
echo "Reloading PostgREST schema cache."
run_sql "notify pgrst, 'reload schema';" >/dev/null
sleep "${NETA_POSTGREST_RELOAD_WAIT_SECONDS:-2}"
}
ensure_migration_table
while IFS='|' read -r migration_id sql_file; do
[ -n "$migration_id" ] || continue
run_migration "$migration_id" "$sql_file"
done <<'SQL_FILES'
0001_schema|supabase/schema.sql
0002_freelancer_os_core|supabase/migrations/0002_add_freelancer_os_core_tables.sql
0003_project_planning_assets|supabase/migrations/0003_add_project_planning_assets.sql
0004_business_os_tables|supabase/migrations/0004_add_business_os_tables.sql
0005_advanced_crm|supabase/migrations/0005_add_advanced_crm_tables.sql
0006_pgvector_embeddings|supabase/migrations/0006_add_pgvector_and_embeddings.sql
0007_client_portal|supabase/migrations/0007_add_client_portal_tables.sql
0008_project_progress_quota|supabase/migrations/0008_add_project_progress_and_quota.sql
0009_first_admin_registration_lock|supabase/migrations/0009_lock_registration_after_first_admin.sql
0010_internal_auth_creation|supabase/migrations/0010_allow_internal_auth_user_creation.sql
0011_service_role_claims_storage|supabase/migrations/0011_fix_service_role_claims_and_storage_policies.sql
SQL_FILES
reload_postgrest_schema_cache
echo "All Neta migrations were applied."
-54
View File
@@ -1,54 +0,0 @@
#!/usr/bin/env node
import crypto from "node:crypto";
const jwtSecret = process.env.JWT_SECRET || randomSecret();
const values = {
NETA_INSTALL_MODE: "full-stack",
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000",
NETA_PORT: process.env.NETA_PORT || "3000",
NEXT_PUBLIC_SUPABASE_URL:
process.env.NEXT_PUBLIC_SUPABASE_URL || "http://localhost:8000",
SUPABASE_API_PORT: process.env.SUPABASE_API_PORT || "8000",
NEXT_PUBLIC_SUPABASE_ANON_KEY:
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || generateSupabaseJwt("anon", jwtSecret),
SUPABASE_SERVICE_ROLE_KEY:
process.env.SUPABASE_SERVICE_ROLE_KEY ||
generateSupabaseJwt("service_role", jwtSecret),
JWT_SECRET: jwtSecret,
POSTGRES_PASSWORD: process.env.POSTGRES_PASSWORD || randomSecret(),
POSTGRES_PORT: process.env.POSTGRES_PORT || "54322",
JWT_EXPIRY: process.env.JWT_EXPIRY || "3600",
SMTP_ADMIN_EMAIL: process.env.SMTP_ADMIN_EMAIL || "admin@neta.local",
};
for (const [key, value] of Object.entries(values)) {
console.log(`${key}=${value}`);
}
function randomSecret() {
return crypto.randomBytes(32).toString("hex");
}
function generateSupabaseJwt(role, secret) {
const header = base64UrlJson({ alg: "HS256", typ: "JWT" });
const payload = base64UrlJson({
iss: "supabase",
ref: "neta",
role,
iat: 1700000000,
exp: 4102444800,
});
const unsigned = `${header}.${payload}`;
const signature = crypto
.createHmac("sha256", secret)
.update(unsigned)
.digest("base64url");
return `${unsigned}.${signature}`;
}
function base64UrlJson(value) {
return Buffer.from(JSON.stringify(value)).toString("base64url");
}
-58
View File
@@ -1,58 +0,0 @@
#!/usr/bin/env sh
set -eu
BACKUP_ROOT="${NETA_BACKUP_DIR:-./backups}"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
BACKUP_DIR="$BACKUP_ROOT/$STAMP"
info() {
printf "%s\n" "$1"
}
fail() {
echo "Error: $1" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || fail "$1 is required."
}
require_container() {
name="$1"
docker inspect "$name" >/dev/null 2>&1 || fail "Container '$name' was not found."
}
require_command docker
require_command tar
require_container neta-db
require_container neta-storage
mkdir -p "$BACKUP_DIR"
BACKUP_DIR_ABS="$(cd "$BACKUP_DIR" && pwd)"
info "Creating Postgres backup"
docker exec neta-db pg_dump \
-U postgres \
-d postgres \
--format=custom \
--clean \
--if-exists \
--file=/tmp/neta-postgres.dump
docker cp neta-db:/tmp/neta-postgres.dump "$BACKUP_DIR_ABS/postgres.dump"
docker exec neta-db rm -f /tmp/neta-postgres.dump
info "Creating Storage backup"
docker cp neta-storage:/var/lib/storage "$BACKUP_DIR_ABS/storage"
tar -czf "$BACKUP_DIR_ABS/storage.tar.gz" -C "$BACKUP_DIR_ABS/storage" .
rm -rf "$BACKUP_DIR_ABS/storage"
cat > "$BACKUP_DIR_ABS/manifest.txt" <<EOF
neta_backup_version=1
created_at_utc=$STAMP
postgres_dump=postgres.dump
storage_archive=storage.tar.gz
EOF
info "Backup created: $BACKUP_DIR_ABS"
-161
View File
@@ -1,161 +0,0 @@
#!/usr/bin/env sh
set -u
FAILURES=0
pass() {
printf "ok %s\n" "$1"
}
fail() {
printf "fail %s\n" "$1"
FAILURES=$((FAILURES + 1))
}
check_container_running() {
name="$1"
if docker inspect -f '{{.State.Running}}' "$name" 2>/dev/null | grep -q true; then
pass "$name is running"
else
fail "$name is not running"
fi
}
check_container_exit_zero() {
name="$1"
if [ "$(docker inspect -f '{{.State.ExitCode}}' "$name" 2>/dev/null)" = "0" ]; then
pass "$name exited successfully"
else
fail "$name did not exit successfully"
fi
}
check_container_health() {
name="$1"
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$name" 2>/dev/null || true)"
if [ "$status" = "healthy" ]; then
pass "$name health is $status"
else
fail "$name health is $status"
fi
}
check_exec() {
label="$1"
shift
if "$@" >/dev/null 2>&1; then
pass "$label"
else
fail "$label"
fi
}
if ! command -v docker >/dev/null 2>&1; then
echo "fail docker is not installed"
exit 1
fi
if ! docker info >/dev/null 2>&1; then
echo "fail docker daemon is not reachable"
exit 1
fi
for container in neta-db neta-auth neta-rest neta-storage neta-supabase-proxy neta-web; do
check_container_running "$container"
done
check_container_exit_zero neta-migrations
check_container_health neta-db
check_container_health neta-web
check_container_health neta-supabase-proxy
check_exec "web health endpoint returns JSON" \
docker exec neta-web wget -qO- http://127.0.0.1:3000/api/health
check_exec "supabase proxy health endpoint responds" \
docker exec neta-supabase-proxy wget -qO- http://127.0.0.1:8000/health
check_exec "auth settings endpoint responds through proxy" \
docker exec neta-supabase-proxy wget -qO- http://127.0.0.1:8000/auth/v1/settings
check_exec "database contains Neta tables" \
docker exec neta-db sh -c "psql -U postgres -d postgres -tAc \"select to_regclass('public.profiles') is not null and to_regclass('public.projects') is not null\" | grep -q t"
if [ "${NETA_DOCTOR_AUTH_SMOKE:-0}" = "1" ]; then
profile_count="$(docker exec neta-db sh -c "psql -U postgres -d postgres -tAc \"select count(*) from public.profiles\"" 2>/dev/null | tr -d "[:space:]")"
if [ "$profile_count" = "0" ]; then
check_exec "auth signup/login smoke succeeds" \
docker exec neta-web node -e '
const apiUrl = process.env.SUPABASE_INTERNAL_URL || process.env.NEXT_PUBLIC_SUPABASE_URL;
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
const email = `doctor-${Date.now()}@neta.local`;
const password = "Test123456!";
const headers = {
apikey: anonKey,
authorization: `Bearer ${anonKey}`,
"content-type": "application/json",
};
async function request(path, body) {
const response = await fetch(`${apiUrl}${path}`, {
method: "POST",
headers,
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`${path} returned ${response.status}: ${await response.text()}`);
}
return response.json();
}
(async () => {
const signup = await request("/auth/v1/signup", { email, password });
const login = await request("/auth/v1/token?grant_type=password", { email, password });
if (!signup.user?.id || !login.access_token || signup.user.id !== login.user?.id) {
throw new Error("signup/login response did not include a matching user and token");
}
})().catch((error) => {
console.error(error.message);
process.exit(1);
});
'
else
check_exec "auth password endpoint rejects invalid login without database errors" \
docker exec neta-web node -e '
const apiUrl = process.env.SUPABASE_INTERNAL_URL || process.env.NEXT_PUBLIC_SUPABASE_URL;
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
const email = `doctor-${Date.now()}@neta.local`;
const headers = {
apikey: anonKey,
authorization: `Bearer ${anonKey}`,
"content-type": "application/json",
};
(async () => {
const response = await fetch(`${apiUrl}/auth/v1/token?grant_type=password`, {
method: "POST",
headers,
body: JSON.stringify({ email, password: "NotARealPassword123!" }),
});
if (response.status >= 500) {
throw new Error(`password endpoint returned ${response.status}: ${await response.text()}`);
}
if (response.ok) {
throw new Error("invalid credentials unexpectedly succeeded");
}
})().catch((error) => {
console.error(error.message);
process.exit(1);
});
'
fi
fi
if [ "$FAILURES" -gt 0 ]; then
printf "\n%d check(s) failed.\n" "$FAILURES"
exit 1
fi
printf "\nAll self-host checks passed.\n"
-73
View File
@@ -1,73 +0,0 @@
#!/usr/bin/env sh
set -eu
BACKUP_DIR="${1:-}"
info() {
printf "%s\n" "$1"
}
fail() {
echo "Error: $1" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || fail "$1 is required."
}
require_container() {
name="$1"
docker inspect "$name" >/dev/null 2>&1 || fail "Container '$name' was not found."
}
if [ -z "$BACKUP_DIR" ]; then
fail "Backup directory is required. Usage: sh ./scripts/selfhost-restore.sh ./backups/20260101T120000Z"
fi
require_command docker
require_command tar
require_container neta-db
require_container neta-storage
[ -f "$BACKUP_DIR/postgres.dump" ] || fail "Missing $BACKUP_DIR/postgres.dump"
[ -f "$BACKUP_DIR/storage.tar.gz" ] || fail "Missing $BACKUP_DIR/storage.tar.gz"
BACKUP_DIR_ABS="$(cd "$BACKUP_DIR" && pwd)"
STORAGE_RESTORE_DIR="$BACKUP_DIR_ABS/.storage-restore"
if [ "${NETA_RESTORE_FORCE:-}" != "1" ]; then
echo "This will overwrite the current full-stack Neta database and local storage."
printf "Type RESTORE to continue: "
read answer
[ "$answer" = "RESTORE" ] || fail "Restore cancelled."
fi
info "Stopping application services"
docker stop neta-web neta-supabase-proxy neta-storage neta-rest neta-auth >/dev/null 2>&1 || true
info "Restoring Postgres"
docker cp "$BACKUP_DIR_ABS/postgres.dump" neta-db:/tmp/neta-postgres-restore.dump
docker exec neta-db pg_restore \
-U postgres \
-d postgres \
--clean \
--if-exists \
/tmp/neta-postgres-restore.dump
docker exec neta-db rm -f /tmp/neta-postgres-restore.dump
info "Restoring Storage"
docker start neta-storage >/dev/null
rm -rf "$STORAGE_RESTORE_DIR"
mkdir -p "$STORAGE_RESTORE_DIR"
tar -xzf "$BACKUP_DIR_ABS/storage.tar.gz" -C "$STORAGE_RESTORE_DIR"
docker exec neta-storage sh -c "rm -rf /var/lib/storage/* /var/lib/storage/.[!.]* /var/lib/storage/..?* 2>/dev/null || true; mkdir -p /var/lib/storage"
docker cp "$STORAGE_RESTORE_DIR/." neta-storage:/var/lib/storage
rm -rf "$STORAGE_RESTORE_DIR"
docker restart neta-storage >/dev/null
info "Starting application services"
docker start neta-auth neta-rest neta-supabase-proxy neta-web >/dev/null
info "Restore completed."