Files
myworkspace/LinkSyncServer/design.md
DavidSaylor fe4cbc3537 feat: add web UI, query engine, session management, and 20 E2E tests
- Web UI: login, dashboard, links CRUD, collections, API keys, admin pages
- Query engine: AND/OR/XOR with field filters, tag search, preview endpoint
- Session management: token expiry detection, 401 interceptor, expiry banner
- Links search: tags included, multi-word AND, query mode with set operations
- Collections: static/dynamic, query builder with preview, public tree view
- Save as Collection: convert search results (static) or query (dynamic)
- Dashboard stats: resilient loading with allSettled pattern
- Login page: redesigned with public collections tree view
- Bug fix: query executor None fields crash (notes/description/url/title)
- E2E tests: 20 Playwright tests covering all critical user flows
- All 104 tests passing (84 unit/integration + 20 E2E)
2026-05-22 07:46:53 -05:00

444 lines
10 KiB
Markdown

# LinkSyncServer - Design Documentation
## Architecture Overview
LinkSyncServer is a Python/FastAPI web application with PostgreSQL as the database, designed for bookmark management with advanced collection and query capabilities.
### Tech Stack
| Component | Technology |
|-----------|------------|
| Framework | FastAPI |
| ORM | SQLAlchemy |
| Authentication | JWT (PyJWT) |
| Database | PostgreSQL 15+ |
| Templates | Jinja2 |
| Static Files | Native static serving |
| Containerization | Docker |
### Directory Structure
```
LinkSyncServer/
├── README.md
├── TODOs.txt
├── design.md
├── tasks.md
├── AGENTS.md
├── docker-compose.yml
├── Dockerfile
├── requirements.txt
├── pyproject.toml
├── app.py
├── config/
│ ├── settings.py
│ └── schema.sql
├── api/
│ ├── __init__.py
│ ├── routes.py
│ ├── endpoints/
│ │ ├── auth.py
│ │ ├── links.py
│ │ ├── collections.py
│ │ └── queries.py
│ ├── parsers/
│ │ ├── __init__.py
│ │ └── query_parser.py
│ └── serializers/
│ ├── __init__.py
│ └── schemas.py
├── models/
│ ├── __init__.py
│ ├── base.py
│ ├── user.py
│ ├── link.py
│ ├── collection.py
│ └── tag.py
├── queries/
│ ├── __init__.py
│ ├── ast.py
│ └── executor.py
├── templates/
│ ├── base.html
│ ├── layout.html
│ ├── links/
│ │ ├── list.html
│ │ ├── detail.html
│ │ └── create.html
│ ├── collections/
│ │ ├── list.html
│ │ ├── detail.html
│ │ ├── create.html
│ │ └── edit.html
│ └── auth/
│ ├── login.html
│ ├── register.html
│ └── forgot_password.html
├── static/
│ ├── css/
│ │ ├── main.css
│ │ └── print.css
│ ├── js/
│ │ ├── main.js
│ │ └── api.js
│ └── images/
└── tests/
├── __init__.py
├── conftest.py
├── test_auth.py
├── test_links.py
└── test_collections.py
```
## Data Models
### User
```python
class User(Base):
id: UUID
username: str (unique, indexed)
email: str (unique, indexed)
password_hash: str
role: Enum('admin', 'user')
is_active: bool
created_at: datetime
updated_at: datetime
```
### Link
```python
class Link(Base):
id: UUID
url: str (indexed)
title: str
description: str | None
notes: str | None
tags: List[UUID] (FK to tags)
favicon_url: str | None
path: str (folder structure)
created_at: datetime
updated_at: datetime
visit_count: int
is_bookmarked: bool
source_set_id: UUID | None (FK to collections)
user_id: UUID (FK, nullable for shared links)
```
### Collection
```python
class Collection(Base):
id: UUID
name: str (unique per user)
description: str | None
query_type: Enum('static', 'dynamic')
query_expression: str | None # SQL-like query string
links: List[UUID] # For static collections only
is_public: bool
created_by: UUID (FK to users)
created_at: datetime
updated_at: datetime
```
### Tag
```python
class Tag(Base):
id: UUID
name: str (unique)
color: str | None
description: str | None
created_at: datetime
updated_at: datetime
```
### AuditLog
```python
class AuditLog(Base):
id: UUID
user_id: UUID (FK, nullable for system events)
action: str
entity_type: str
entity_id: UUID
old_value: str | None
new_value: str | None
ip_address: str
created_at: datetime
```
## Query Engine Design
### Query Syntax
```
('term1', 'term2') OR tagA AND tagB XOR url:example.com
```
### Parser Architecture
```
Input String
Tokenize
Build AST Node Tree
Validate AST
Serialize to JSON (for storage)
```
### AST Node Types
| Node Type | Description |
|-----------|-------------|
| `TermSet` | Tuple of search terms: `('term1', 'term2')` |
| `TagFilter` | Tag-based filter: `tagA` |
| `FieldFilter` | Field value filter: `url:example.com` |
| `AND` | Set intersection |
| `OR` | Set union |
| `XOR` | Set difference |
| `NOT` | Negation |
### Executor Flow
```
1. Parse query expression
2. Validate AST
3. Build SQL from AST
4. Execute against PostgreSQL
5. Return result set
6. Serialize for client
```
### Full-Text Search
PostgreSQL full-text search enabled via:
```sql
CREATE INDEX links_fts_idx ON links USING GIN (to_tsvector('english', url || ' ' || title || ' ' || description || ' ' || notes));
```
Query terms converted to tsquery and matched.
## API Design
### Authentication Flow
```
1. POST /api/auth/register/ - Create new account
2. POST /api/auth/login/ - Get JWT token
3. Include Authorization header in all API requests
4. Token validated per request
```
### Link Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/links/ | List links (paginated) |
| GET | /api/links/{id}/ | Get link details |
| POST | /api/links/ | Create link |
| PUT | /api/links/{id}/ | Update link |
| DELETE | /api/links/{id}/ | Delete link |
### Collection Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/collections/ | List collections |
| GET | /api/collections/{id}/ | Get collection |
| POST | /api/collections/ | Create collection |
| PUT | /api/collections/{id}/ | Update collection |
| DELETE | /api/collections/{id}/ | Delete collection |
| POST | /api/collections/{id}/refresh/ | Re-evaluate dynamic |
### Query Execution Endpoint
```
POST /api/queries/execute/
{
"expression": "('work', 'dev') OR tag:work",
"static_collections": [...]
}
→ Returns filtered link list
```
## Security Design
### Password Storage
- bcrypt hashing with cost factor 12
- Passwords never logged or exposed
### JWT Tokens
- HS256 algorithm
- 24-hour expiration
- Refresh token pattern for long sessions
### Rate Limiting
- 100 requests per minute per IP
- 10 login attempts per hour per IP
### CORS
- Configurable origin whitelist
- Credentials allowed for extension
## Docker Compose Design
### Services
| Service | Image | Purpose |
|---------|-------|---------|
| web | built from Dockerfile | FastAPI app |
| db | postgres:15-alpine | PostgreSQL database |
### Environment Variables
```
DATABASE_URL=postgresql://linksync:password@db:5432/linksync
SECRET_KEY=<generated>
ADMIN_USERNAME=admin
ADMIN_PASSWORD=<password>
DEBUG=False
HOST=0.0.0.0
PORT=5000
CORS_ORIGINS=http://localhost:5555
```
### Health Checks
```yaml
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 30s
timeout: 10s
retries: 3
```
## Sync Protocol Design
### Sync Endpoint
```
POST /api/sync/
Request:
{
"type": "bi-directional|browser-authoritative|server-authoritative",
"deletions_enabled": true,
"links": [
{
"id": "uuid",
"url": "https://...",
"title": "...",
... all fields
}
]
}
Response:
{
"actions": [
{"type": "create", "link_id": "..."},
{"type": "update", "link_id": "..."},
{"type": "delete", "link_id": "..."}
]
}
```
### Conflict Resolution
Priority based on sync mode:
1. **Bi-directional**: Keep both versions, merge metadata
2. **Browser Authoritative**: Overwrite with browser data
3. **Server Authoritative**: Download only, no overwrites
## Template Design
### Layout Components
```html
<!-- base.html -->
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}LinkSync{% endblock %}</title>
<link rel="stylesheet" href="/static/css/main.css">
</head>
<body>
<nav>{% block nav %}{% endblock %}</nav>
<main>{% block content %}{% endblock %}</main>
<footer>{% block footer %}{% endblock %}</footer>
<script src="/static/js/main.js"></script>
</body>
</html>
```
### Responsive Design
- Mobile-first CSS
- Breakpoints: 768px, 1024px
- Touch-friendly UI controls
## UI/UX Design
### Session Management
#### Token Lifecycle
1. **Login** - stores JWT in localStorage with user info
2. **Page load** - checks token expiry via JWT `exp` claim
3. **API calls** - validates expiry before each request
4. **401 response** - clears storage and redirects to `/login?expired=1`
5. **Expiry warning** - shows banner if token expires in <2 minutes
#### Implementation
- `main.js`: `isTokenExpired()`, `getTokenExpirySeconds()`, `redirectToLogin()`
- `apiFetch()`: Pre-flight expiry check + 401 interceptor
- `dashboard.js`: Expiry warning banner on page load
- `login.html`: Shows expiry message when redirected with `?expired=1`
### Search Architecture
#### Simple Search (Links Page)
- **Input**: Free text with space-separated terms
- **Behavior**: Each term must match at least one field (AND logic)
- **Fields searched**: title, URL, description, notes, tags
- **Backend**: Multiple `OR` clauses per term, combined with `AND` between terms
#### Query Mode (Links Page + Collections)
- **Input**: Query expression with operators
- **Syntax**: `term1 AND term2 OR term3 NOT term4`
- **Operators**: AND (intersection), OR (union), XOR (difference), NOT (negation)
- **Parentheses**: Grouping for precedence control
- **Field filters**: `tag:value`, `url:value`, `title:value`, `description:value`, `path:value`
- **Term sets**: `(term1, term2)` matches any term in the set
### Collection Query Builder
#### Dynamic Collection Modal
- **Type selector** toggles query section visibility
- **Query input** with syntax hint
- **Preview button** fetches matching links via `/api/queries/preview`
- **Results display** shows count and first 10 matching links
- **Save** stores `query_expression: { expression: "..." }` for dynamic collections
#### Save as Collection (Links Page)
- **Static mode**: Saves current result set link IDs
- **Dynamic mode**: Saves the active query expression
- **Modal** pre-fills name based on current search/query
- **Public toggle** controls collection visibility
### Error Handling
- **API errors**: Structured error responses with user-friendly messages
- **Network errors**: Graceful fallbacks with retry options
- **Form validation**: Client-side validation before submission
- **Server errors**: Logged with stack traces in debug mode