Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
1a5e17c
feat: Transcribe auth
viperehonchuk May 20, 2026
35ffc23
fix: make check-pr generate Kysely types
viperehonchuk May 20, 2026
cf4f7c9
fix Kysely codegen run script
viperehonchuk May 20, 2026
3c09f93
fix Kysely codegen run script, 2
viperehonchuk May 20, 2026
35a52df
fix Kysely codegen run directory
viperehonchuk May 20, 2026
9e404ca
fix: DATABASE_URL env var
viperehonchuk May 20, 2026
a51159a
fix: DATABASE_URL env var, 2
viperehonchuk May 20, 2026
646856b
feat: better login UI
viperehonchuk May 20, 2026
14791b9
feat: list projects
viperehonchuk May 20, 2026
854bacc
feat: implement project create endpoint and database logic
viperehonchuk May 20, 2026
f5c91d8
style: apply linter fixes to imports and variables
viperehonchuk May 20, 2026
aaa4b8a
fix: resolve ESLint errors by adding types and replacing deprecated Z…
viperehonchuk May 20, 2026
c021425
style: fix import spacing in create-project.ts
viperehonchuk May 20, 2026
ea25a35
fix project create
viperehonchuk May 20, 2026
afa3aa6
feat: add project create page
viperehonchuk May 20, 2026
59378c4
fix: resolve typescript errors in project create page
viperehonchuk May 20, 2026
d8bd26a
style: apply linter fixes to project create page
viperehonchuk May 20, 2026
d609f96
project create form
viperehonchuk May 20, 2026
9b847de
feat: add project create link to transcribe page
viperehonchuk May 20, 2026
039d0b3
style: fix import spacing in transcribe page
viperehonchuk May 20, 2026
12f101b
fix: create form
viperehonchuk May 20, 2026
76360c9
gha review
viperehonchuk May 21, 2026
1ef65d4
Merge branch 'main' into feat/transcribe
viperehonchuk May 22, 2026
8b15c4d
refactor: use requestApi helper in project creation
viperehonchuk May 22, 2026
0ee9c92
refactor: move inline styles to CSS module
viperehonchuk May 22, 2026
2a026b8
style: format imports
viperehonchuk May 22, 2026
a9ab56b
fix: address review
viperehonchuk May 22, 2026
623be22
Merge branch 'main' into feat/transcribe
viperehonchuk May 22, 2026
0f18855
Merge branch 'main' into feat/transcribe
viperehonchuk May 22, 2026
552f2bc
Merge branch 'main' into feat/transcribe
viperehonchuk May 22, 2026
9b63167
Merge branch 'main' into feat/transcribe
viperehonchuk May 22, 2026
634f091
Merge branch 'main' into feat/transcribe
viperehonchuk May 22, 2026
4f5fa88
Merge branch 'main' into feat/transcribe
viperehonchuk May 22, 2026
0149ae8
Merge branch 'main' into feat/transcribe
viperehonchuk May 27, 2026
1fd8bb3
Merge branch 'main' into feat/transcribe
viperehonchuk May 29, 2026
c43a0e0
fix: address review issues
viperehonchuk May 29, 2026
38d8a97
Merge branch 'main' into feat/transcribe
viperehonchuk May 29, 2026
3fd4867
feat(server): support local SQLite fallback for development
viperehonchuk May 30, 2026
f7f09c7
chore: remove github oauth client configuration and update setup docu…
viperehonchuk May 30, 2026
7ab1d18
chore: update environment configuration and setup instructions
viperehonchuk May 30, 2026
4849c39
Merge branch 'main' into feat/transcribe
viperehonchuk Jun 4, 2026
e097994
Merge branch 'main' into feat/transcribe
viperehonchuk Jun 5, 2026
a6a1f98
Merge branch 'main' into feat/transcribe
viperehonchuk Jun 5, 2026
3382b4b
Merge branch 'main' into feat/transcribe
viperehonchuk Jun 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ NEXT_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com
NEXT_PUBLIC_GITHUB_REPO=undead404/koreni
NEXT_PUBLIC_GISCUS_REPO_ID=
NEXT_PUBLIC_GISCUS_CATEGORY_ID=
GEMINI_API_KEY=
GEMINI_API_KEY=
NEXT_PUBLIC_OAUTH_CLIENT_ID=
OAUTH_CLIENT_SECRET=
NEXT_PUBLIC_API_SITE=http://localhost:4000
6 changes: 6 additions & 0 deletions .github/workflows/check-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ jobs:
run: yarn --frozen-lockfile --prefer-offline
working-directory: src/server

- name: Generate Kysely types
run: yarn exec kysely-codegen --dialect libsql
working-directory: src/server
env:
DATABASE_URL: '${{ vars.TURSO_DATABASE_URL }}?authToken=${{ secrets.TURSO_DATABASE_TOKEN }}'

- name: Lint
run: yarn lint

Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,6 @@ jobs:
echo "NEXT_PUBLIC_SITE=${{ vars.SITE }}" >> .env
echo "NODE_ENV=production" >> .env
echo "VALID_API_KEYS=${{ vars.VALID_API_KEYS }}" >> .env
echo "GITHUB_OAUTH_CLIENT_ID=${{ secrets.OAUTH_CLIENT_ID }}" >> .env
echo "GITHUB_OAUTH_CLIENT_SECRET=${{ secrets.OAUTH_CLIENT_SECRET }}" >> .env
echo "FALLBACK_EMAIL=${{ secrets.FALLBACK_EMAIL }}" >> .env

# Install dependencies ON THE SERVER
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,6 @@ next-env.d.ts
.gemini/

# GitHub App credentials
gha-creds-*.json
gha-creds-*.json

/src/server/local.db
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,40 @@ All other contents (the code and everything else) in this repository is licensed
2. Yarn
3. Docker

The easiest way is to setup Typesense with Docker. Run this:

```sh
yarn
yarn docker:typesense:start
yarn docker:typesense:start # If you want to use TypeSense search
yarn typesense:populate # This takes substantial time
cd ./src/server && yarn db:init && cd ../..
cp ./.env.example ./.env
cp ./src/server/.env.example ./src/server/.env
```

In `./src/server/.env`, you need to set a strong JWT_SECRET value. To generate it, you can use this command:

```sh
# Source - https://stackoverflow.com/a/74481179
# Retrieved 2026-05-30, License - CC BY-SA 4.0
node -e "console.log(require('crypto').randomBytes(32).toString('hex'));"
```

To set `NEXT_PUBLIC_OAUTH_CLIENT_ID` in `./.env` and `OAUTH_CLIENT_ID` in `./src/server/.env`, ask the maintainer - @undead404.

Then, to run the app:

```sh
yarn dev
```

If you want to work with data contribution and-or transcription, run in another terminal:

```sh
yarn dev:server
```

## Production setup

Use scripts from scripts/prepare-server folder.
Use scripts from `./scripts/prepare-server` folder.

## Clean repo

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"@bugsnag/plugin-react": "^8.2.0",
"@hookform/error-message": "^2.0.1",
"@hookform/resolvers": "^5.2.2",
"@react-oauth/google": "^0.13.5",
"clsx": "^2.1.1",
"dotenv": "~16.4.7",
"leaflet": "~1.9.4",
Expand Down
43 changes: 43 additions & 0 deletions specs/039-project-create.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Project create

## Pathname

Create a new component at `src/app/transcribe/create`.

## UX

Add form to create a new project.

The required schema is defined in `src/server/src/schemata.ts` – `projectCreatePayloadSchema`.

Fields:

- Title: text input, required
- ID: text input, required, unique across all projects
- isHandwritten: select (unset, handwritten, typed), required
- Location: coordinates of the settlement relevant to the project, required. May copy `src/app/components/contribute/spatial-input.tsx`
- Sources: URLs relevant to the project, required. May copy `src/app/components/contribute/sources-input.tsx`
- Table locale: select (unset, pl, ru, uk), required
- Years range: range of years relevant to the project, or a single year, required. May copy `src/app/components/contribute/years-input.tsx`

## Request

Send a POST request to `new URL('/api/transcribe/projects', environment.NEXT_PUBLIC_API_SITE)`.

### Request body

```http
POST /api/transcribe/projects HTTP/1.1
Host: localhost:4000
Content-Type: application/json

{
"id": "test",
"isHandwritten": true,
"location": [0, 0],
"sources": ["https://example/com/data"],
"tableLocale": "pl",
"title": "Test",
"yearsRange": [1900, 2000]
}
```
3 changes: 3 additions & 0 deletions specs/040-project-create-link.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Project create link

Add a link to the project creation page (`/transcribe/create`) at `src/app/transcribe/page.tsx`, above the projects list.
8 changes: 8 additions & 0 deletions specs/041-project-create-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Project create crash

## Error text

```plain
TypeError: Cannot read properties of undefined (reading 'join')
at Object.render (page.tsx:164:38)
```
118 changes: 118 additions & 0 deletions specs/068-local-sqlite-fallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
description: Update database configuration to support local SQLite fallback for development without Turso credentials.
status: draft
targets:
- src/server/src/database/client.ts
- src/server/.env.example
- src/server/src/environment.ts
- src/server/package.json
- src/server/src/database/init.ts
context:
- src/server/src/database/schema.sql
---

# Local SQLite Fallback for Development

## 1. Architectural Boundary

- **Execution Context:** Server (Hono)
- **Data Scope:** SQLite (Kysely)

---

## 2. State Transition Matrix

### Fault / Current State

- **Condition:** A new developer clones the repository and attempts to start the backend locally.
- **Behavior:** The application crashes during environment variable validation because `TURSO_DATABASE_URL` is enforced as a URL and `TURSO_DATABASE_TOKEN` is a mandatory non-empty string.
- **Log/Trace:**

```ts
ZodError: [
{
code: 'invalid_type',
expected: 'string',
received: 'undefined',
path: ['TURSO_DATABASE_URL'],
message: 'Required',
},
{
code: 'custom',
message: 'String cannot be empty',
path: ['TURSO_DATABASE_TOKEN'],
},
];
```

### Target / Resolved State

- **Condition:** A new developer starts the application locally with an empty `.env` or defaults from `.env.example`.
- **Behavior:** The application falls back to a local SQLite database file (e.g., `file:local.db`) and bypasses the requirement for a Turso authentication token. The developer can initialize the local database schema using a new `db:init` script.
- **Schema/Type Alteration:**

```ts
const environmentSchema = z.object({
// ...
TURSO_DATABASE_URL: z.string().default('file:local.db'),
TURSO_DATABASE_TOKEN: nonEmptyString.optional(),
});
```

---

## 3. Execution Pipeline

### 3.1. src/server/src/environment.ts

1. Modify the `TURSO_DATABASE_URL` validation in `environmentSchema` to support local file connections (e.g., `z.string()`) and provide a default value of `'file:local.db'`.
2. Update `TURSO_DATABASE_TOKEN` to be optional using `.optional()`.

### 3.2. src/server/.env.example

1. Update the example configuration to indicate that `TURSO_DATABASE_URL` and `TURSO_DATABASE_TOKEN` can be omitted for local development to use a local SQLite instance.

### 3.3. src/server/src/database/client.ts

1. Update the `createClient` configuration object to safely pass `environment.TURSO_DATABASE_TOKEN` only if it is defined, preventing validation errors from `@libsql/client` when running without a remote auth token.

### 3.4. src/server/package.json

1. Add a `"db:init"` script (e.g., `"db:init": "tsx src/database/init.ts"`) that developers can run to seed the local SQLite database file with the correct initial schema structure after cloning the project.

### 3.5. src/server/src/database/init.ts

1. Create a script that utilizes the existing `better-sqlite3` and `fs` packages.
2. The script should read the contents of `src/server/src/database/schema.sql`.
3. It should instantiate a new `Database('local.db')` and execute the SQL schema string to initialize the database structure.

---

## 4. Hard Constraints

- **Backend ESM:** All relative imports in Hono/Node.js files MUST terminate with explicit `.js` extensions.
- **Isolation:** Do not modify schemas, context files, or unrelated components not explicitly listed in the `targets` frontmatter.

---

## 5. Agentic Verification

Execute the following commands to validate the implementation:

1. **Type & Lint Pass:** Run standard formatting and type checks.

```opencode
/lint
```

2. **Targeted Test Execution:** Run the specific route or backend test.

```opencode
/test-server
```

3. **ESM Validation (Backend Only):**

```opencode
/verify-esm src/server/src/database/client.ts
```
2 changes: 0 additions & 2 deletions src/app/components/contribute/location-picker.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
'use client';

import type { Marker as LMarker } from 'leaflet';
Comment thread
undead404 marked this conversation as resolved.
import { usePostHog } from 'posthog-js/react';
import { memo, useMemo, useRef } from 'react';
Expand Down
5 changes: 5 additions & 0 deletions src/app/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ const environment = {
process.env.NEXT_PUBLIC_LOCATIONIQ_KEY,
false,
),
NEXT_PUBLIC_OAUTH_CLIENT_ID: validateEnvironment(
'NEXT_PUBLIC_OAUTH_CLIENT_ID',
process.env.NEXT_PUBLIC_OAUTH_CLIENT_ID,
true,
),
NEXT_PUBLIC_POSTHOG_KEY: validateEnvironment(
'NEXT_PUBLIC_POSTHOG_KEY',
process.env.NEXT_PUBLIC_POSTHOG_KEY,
Expand Down
2 changes: 1 addition & 1 deletion src/app/helpers/generate-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ export function generateJsonLd(item: IndexationTable): string {
],
};

return process.env.NODE_ENV === 'development'
return environment.NODE_ENV === 'development'
? JSON.stringify(jsonLd, null, 2)
: JSON.stringify(jsonLd);
}
17 changes: 17 additions & 0 deletions src/app/transcribe/components/logout-button.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.root {
display: inline-flex;
padding: 0.25rem 0.5rem;
background-color: #dc2626;
color: #fff;
border-radius: 0.25rem;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
text-decoration: none;
cursor: pointer;
user-select: none;
transition:
background-color 0.2s ease-in-out,
border-color 0.2s ease-in-out,
color 0.2s ease-in-out;
}
36 changes: 36 additions & 0 deletions src/app/transcribe/components/logout-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use client';

import { googleLogout } from '@react-oauth/google';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';

import requestApi from '../services/api';

import styles from './logout-button.module.css';

export default function LogoutButton() {
const router = useRouter();

const handleLogout = () => {
// 1. Sever the local Google Identity SDK state
googleLogout();

// 2. Execute backend cookie destruction
requestApi('/api/auth/session/current', {
method: 'DELETE',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unclear usage of REST standard. Use one of options:

  • DELETE:/api/auth/session - still tricky to understand, and not best practice, but at least readable
  • GET:/api/auth/signout - I'd mark it as most used option

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing to DELETE api/auth/session/current. GET is a no-go.

})
.then(() => {
router.push('/transcribe/login');
return;
})
.catch(() => {
toast.error('Failed to log out');
});
};

return (
<button onClick={handleLogout} className={styles.root}>
Log Out
</button>
);
}
32 changes: 32 additions & 0 deletions src/app/transcribe/components/projects-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use client';

import { useEffect, useState } from 'react';
import { toast } from 'sonner';

import { type Project, projectResponseSchema } from '../schemata';
import requestApi from '../services/api';

export default function ProjectsList() {
const [projects, setProjects] = useState<Project[]>([]);
useEffect(() => {
requestApi('/api/transcribe/projects')
.then((response) => response.json())
Comment thread
undead404 marked this conversation as resolved.
.then((data: unknown) => {
const projectsData = projectResponseSchema.parse(data);
setProjects(projectsData.projects);
return;
})
.catch(() => {
toast.error('Error loading projects');
});
}, []);
return (
<section>
<h1>Projects</h1>
{projects.map((project) => (
<p key={project.id}>{project.title}</p>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's expected to render

not ? just not sure about logic, but looks like it's a place where you render list of projects, and potentially can open one of them

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just a placeholder for now; the implementation is the next stage – see #694

))}
{projects.length === 0 && <p>No projects</p>}
</section>
);
}
8 changes: 8 additions & 0 deletions src/app/transcribe/components/transcribe-header.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.root {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 1rem;
background-color: #fff;
border-bottom: 1px solid #e5e7eb;
}
Loading
Loading