docs: add all-future-features implementation plan
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -23,6 +23,9 @@ worktrees/
|
|||||||
# TypeScript incremental build info
|
# TypeScript incremental build info
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Local scratch files
|
||||||
|
.tmp-*
|
||||||
|
|
||||||
# Test binary
|
# Test binary
|
||||||
*.test
|
*.test
|
||||||
|
|
||||||
|
|||||||
665
.tmp-render.yaml
665
.tmp-render.yaml
@@ -1,663 +1,2 @@
|
|||||||
---
|
# Temporary file used during local helm rendering.
|
||||||
# Source: tline/templates/secret.yaml.tpl
|
# This file is intentionally empty in-repo; real rendered output should not be committed.
|
||||||
apiVersion: v1
|
|
||||||
kind: Secret
|
|
||||||
metadata:
|
|
||||||
name: tline-tline-secrets
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/managed-by: Helm
|
|
||||||
helm.sh/chart: "tline-0.1.0"
|
|
||||||
type: Opaque
|
|
||||||
data:
|
|
||||||
POSTGRES_PASSWORD: Y2hhbmdlLW1l
|
|
||||||
MINIO_ACCESS_KEY_ID: bWluaW9hZG1pbg==
|
|
||||||
MINIO_SECRET_ACCESS_KEY: bWluaW9hZG1pbg==---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Secret
|
|
||||||
metadata:
|
|
||||||
name: tline-tline-registry
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/managed-by: Helm
|
|
||||||
helm.sh/chart: "tline-0.1.0"
|
|
||||||
type: kubernetes.io/dockerconfigjson
|
|
||||||
data:
|
|
||||||
.dockerconfigjson: eyJhdXRocyI6eyJyZWdpc3RyeS5sYW46NTAwMCI6eyJhdXRoIjoiZFRwdyIsImVtYWlsIjoiZUBleGFtcGxlLmNvbSIsInBhc3N3b3JkIjoicCIsInVzZXJuYW1lIjoidSJ9fX0=
|
|
||||||
---
|
|
||||||
# Source: tline/templates/configmap.yaml.tpl
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: tline-tline-config
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/managed-by: Helm
|
|
||||||
helm.sh/chart: "tline-0.1.0"
|
|
||||||
data:
|
|
||||||
APP_NAME: "flux"
|
|
||||||
NEXT_PUBLIC_APP_NAME: "flux"
|
|
||||||
QUEUE_NAME: "tline"
|
|
||||||
DATABASE_URL: "postgres://tline:change-me@tline-tline-postgres:5432/tline"
|
|
||||||
REDIS_URL: "redis://tline-tline-redis:6379"
|
|
||||||
MINIO_INTERNAL_ENDPOINT: "http://tline-tline-minio:9000"
|
|
||||||
MINIO_PUBLIC_ENDPOINT_TS: "https://minio.tailxyz.ts.net"
|
|
||||||
MINIO_REGION: "us-east-1"
|
|
||||||
MINIO_BUCKET: "media"
|
|
||||||
MINIO_PRESIGN_EXPIRES_SECONDS: "900"
|
|
||||||
---
|
|
||||||
# Source: tline/templates/minio.yaml.tpl
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: tline-tline-minio
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/managed-by: Helm
|
|
||||||
helm.sh/chart: "tline-0.1.0"
|
|
||||||
app.kubernetes.io/component: minio
|
|
||||||
spec:
|
|
||||||
type: ClusterIP
|
|
||||||
ports:
|
|
||||||
- name: s3
|
|
||||||
port: 9000
|
|
||||||
targetPort: s3
|
|
||||||
- name: console
|
|
||||||
port: 9001
|
|
||||||
targetPort: console
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/component: minio
|
|
||||||
---
|
|
||||||
# Source: tline/templates/postgres.yaml.tpl
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: tline-tline-postgres
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/managed-by: Helm
|
|
||||||
helm.sh/chart: "tline-0.1.0"
|
|
||||||
spec:
|
|
||||||
type: ClusterIP
|
|
||||||
ports:
|
|
||||||
- name: postgres
|
|
||||||
port: 5432
|
|
||||||
targetPort: postgres
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/component: postgres
|
|
||||||
---
|
|
||||||
# Source: tline/templates/redis.yaml.tpl
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: tline-tline-redis
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/managed-by: Helm
|
|
||||||
helm.sh/chart: "tline-0.1.0"
|
|
||||||
spec:
|
|
||||||
type: ClusterIP
|
|
||||||
ports:
|
|
||||||
- name: redis
|
|
||||||
port: 6379
|
|
||||||
targetPort: redis
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/component: redis
|
|
||||||
---
|
|
||||||
# Source: tline/templates/web.yaml.tpl
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: tline-tline-web
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/managed-by: Helm
|
|
||||||
helm.sh/chart: "tline-0.1.0"
|
|
||||||
app.kubernetes.io/component: web
|
|
||||||
spec:
|
|
||||||
type: ClusterIP
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
port: 3000
|
|
||||||
targetPort: http
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/component: web
|
|
||||||
---
|
|
||||||
# Source: tline/templates/redis.yaml.tpl
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: tline-tline-redis
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/managed-by: Helm
|
|
||||||
helm.sh/chart: "tline-0.1.0"
|
|
||||||
app.kubernetes.io/component: redis
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/component: redis
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/component: redis
|
|
||||||
spec:
|
|
||||||
imagePullSecrets:
|
|
||||||
- name: "tline-tline-registry"
|
|
||||||
affinity:
|
|
||||||
nodeAffinity:
|
|
||||||
requiredDuringSchedulingIgnoredDuringExecution:
|
|
||||||
nodeSelectorTerms:
|
|
||||||
- matchExpressions:
|
|
||||||
- key: node-class
|
|
||||||
operator: In
|
|
||||||
values:
|
|
||||||
- compute
|
|
||||||
containers:
|
|
||||||
- name: redis
|
|
||||||
image: "redis:7"
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
ports:
|
|
||||||
- name: redis
|
|
||||||
containerPort: 6379
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: 300m
|
|
||||||
memory: 512Mi
|
|
||||||
requests:
|
|
||||||
cpu: 50m
|
|
||||||
memory: 128Mi
|
|
||||||
---
|
|
||||||
# Source: tline/templates/web.yaml.tpl
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: tline-tline-web
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/managed-by: Helm
|
|
||||||
helm.sh/chart: "tline-0.1.0"
|
|
||||||
app.kubernetes.io/component: web
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/component: web
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/component: web
|
|
||||||
spec:
|
|
||||||
imagePullSecrets:
|
|
||||||
- name: "tline-tline-registry"
|
|
||||||
affinity:
|
|
||||||
nodeAffinity:
|
|
||||||
requiredDuringSchedulingIgnoredDuringExecution:
|
|
||||||
nodeSelectorTerms:
|
|
||||||
- matchExpressions:
|
|
||||||
- key: node-class
|
|
||||||
operator: In
|
|
||||||
values:
|
|
||||||
- compute
|
|
||||||
containers:
|
|
||||||
- name: web
|
|
||||||
image: "registry.lan:5000/tline-web:dev"
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
containerPort: 3000
|
|
||||||
envFrom:
|
|
||||||
- configMapRef:
|
|
||||||
name: tline-tline-config
|
|
||||||
env:
|
|
||||||
- name: POSTGRES_PASSWORD
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: tline-tline-secrets
|
|
||||||
key: POSTGRES_PASSWORD
|
|
||||||
- name: MINIO_ACCESS_KEY_ID
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: tline-tline-secrets
|
|
||||||
key: MINIO_ACCESS_KEY_ID
|
|
||||||
- name: MINIO_SECRET_ACCESS_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: tline-tline-secrets
|
|
||||||
key: MINIO_SECRET_ACCESS_KEY
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /api/healthz
|
|
||||||
port: http
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 5
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /api/healthz
|
|
||||||
port: http
|
|
||||||
initialDelaySeconds: 20
|
|
||||||
periodSeconds: 10
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: 1000m
|
|
||||||
memory: 1Gi
|
|
||||||
requests:
|
|
||||||
cpu: 200m
|
|
||||||
memory: 256Mi
|
|
||||||
---
|
|
||||||
# Source: tline/templates/worker.yaml.tpl
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: tline-tline-worker
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/managed-by: Helm
|
|
||||||
helm.sh/chart: "tline-0.1.0"
|
|
||||||
app.kubernetes.io/component: worker
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/component: worker
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/component: worker
|
|
||||||
spec:
|
|
||||||
imagePullSecrets:
|
|
||||||
- name: "tline-tline-registry"
|
|
||||||
affinity:
|
|
||||||
nodeAffinity:
|
|
||||||
requiredDuringSchedulingIgnoredDuringExecution:
|
|
||||||
nodeSelectorTerms:
|
|
||||||
- matchExpressions:
|
|
||||||
- key: node-class
|
|
||||||
operator: In
|
|
||||||
values:
|
|
||||||
- compute
|
|
||||||
containers:
|
|
||||||
- name: worker
|
|
||||||
image: "registry.lan:5000/tline-worker:dev"
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
envFrom:
|
|
||||||
- configMapRef:
|
|
||||||
name: tline-tline-config
|
|
||||||
env:
|
|
||||||
- name: POSTGRES_PASSWORD
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: tline-tline-secrets
|
|
||||||
key: POSTGRES_PASSWORD
|
|
||||||
- name: MINIO_ACCESS_KEY_ID
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: tline-tline-secrets
|
|
||||||
key: MINIO_ACCESS_KEY_ID
|
|
||||||
- name: MINIO_SECRET_ACCESS_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: tline-tline-secrets
|
|
||||||
key: MINIO_SECRET_ACCESS_KEY
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: 2000m
|
|
||||||
memory: 2Gi
|
|
||||||
requests:
|
|
||||||
cpu: 500m
|
|
||||||
memory: 1Gi
|
|
||||||
---
|
|
||||||
# Source: tline/templates/minio.yaml.tpl
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: StatefulSet
|
|
||||||
metadata:
|
|
||||||
name: tline-tline-minio
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/managed-by: Helm
|
|
||||||
helm.sh/chart: "tline-0.1.0"
|
|
||||||
app.kubernetes.io/component: minio
|
|
||||||
spec:
|
|
||||||
serviceName: tline-tline-minio
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/component: minio
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/component: minio
|
|
||||||
spec:
|
|
||||||
imagePullSecrets:
|
|
||||||
- name: "tline-tline-registry"
|
|
||||||
affinity:
|
|
||||||
nodeAffinity:
|
|
||||||
requiredDuringSchedulingIgnoredDuringExecution:
|
|
||||||
nodeSelectorTerms:
|
|
||||||
- matchExpressions:
|
|
||||||
- key: node-class
|
|
||||||
operator: In
|
|
||||||
values:
|
|
||||||
- compute
|
|
||||||
containers:
|
|
||||||
- name: minio
|
|
||||||
image: "minio/minio:RELEASE.2024-01-16T16-07-38Z"
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
args:
|
|
||||||
- server
|
|
||||||
- /data
|
|
||||||
- "--console-address=:9001"
|
|
||||||
ports:
|
|
||||||
- name: s3
|
|
||||||
containerPort: 9000
|
|
||||||
- name: console
|
|
||||||
containerPort: 9001
|
|
||||||
env:
|
|
||||||
- name: MINIO_ROOT_USER
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: tline-tline-secrets
|
|
||||||
key: MINIO_ACCESS_KEY_ID
|
|
||||||
- name: MINIO_ROOT_PASSWORD
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: tline-tline-secrets
|
|
||||||
key: MINIO_SECRET_ACCESS_KEY
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /minio/health/ready
|
|
||||||
port: s3
|
|
||||||
initialDelaySeconds: 10
|
|
||||||
periodSeconds: 5
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /minio/health/live
|
|
||||||
port: s3
|
|
||||||
initialDelaySeconds: 20
|
|
||||||
periodSeconds: 10
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: 1500m
|
|
||||||
memory: 2Gi
|
|
||||||
requests:
|
|
||||||
cpu: 250m
|
|
||||||
memory: 512Mi
|
|
||||||
volumeMounts:
|
|
||||||
- name: data
|
|
||||||
mountPath: /data
|
|
||||||
volumeClaimTemplates:
|
|
||||||
- metadata:
|
|
||||||
name: data
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: "200Gi"
|
|
||||||
---
|
|
||||||
# Source: tline/templates/postgres.yaml.tpl
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: StatefulSet
|
|
||||||
metadata:
|
|
||||||
name: tline-tline-postgres
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/managed-by: Helm
|
|
||||||
helm.sh/chart: "tline-0.1.0"
|
|
||||||
app.kubernetes.io/component: postgres
|
|
||||||
spec:
|
|
||||||
serviceName: tline-tline-postgres
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/component: postgres
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/component: postgres
|
|
||||||
spec:
|
|
||||||
imagePullSecrets:
|
|
||||||
- name: "tline-tline-registry"
|
|
||||||
affinity:
|
|
||||||
nodeAffinity:
|
|
||||||
requiredDuringSchedulingIgnoredDuringExecution:
|
|
||||||
nodeSelectorTerms:
|
|
||||||
- matchExpressions:
|
|
||||||
- key: node-class
|
|
||||||
operator: In
|
|
||||||
values:
|
|
||||||
- compute
|
|
||||||
containers:
|
|
||||||
- name: postgres
|
|
||||||
image: "postgres:16"
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
ports:
|
|
||||||
- name: postgres
|
|
||||||
containerPort: 5432
|
|
||||||
env:
|
|
||||||
- name: POSTGRES_USER
|
|
||||||
value: "tline"
|
|
||||||
- name: POSTGRES_DB
|
|
||||||
value: "tline"
|
|
||||||
- name: POSTGRES_PASSWORD
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: tline-tline-secrets
|
|
||||||
key: POSTGRES_PASSWORD
|
|
||||||
readinessProbe:
|
|
||||||
exec:
|
|
||||||
command:
|
|
||||||
- sh
|
|
||||||
- -c
|
|
||||||
- pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 5
|
|
||||||
livenessProbe:
|
|
||||||
exec:
|
|
||||||
command:
|
|
||||||
- sh
|
|
||||||
- -c
|
|
||||||
- pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"
|
|
||||||
initialDelaySeconds: 20
|
|
||||||
periodSeconds: 10
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: 1500m
|
|
||||||
memory: 2Gi
|
|
||||||
requests:
|
|
||||||
cpu: 500m
|
|
||||||
memory: 1Gi
|
|
||||||
volumeMounts:
|
|
||||||
- name: data
|
|
||||||
mountPath: /var/lib/postgresql/data
|
|
||||||
volumeClaimTemplates:
|
|
||||||
- metadata:
|
|
||||||
name: data
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: "20Gi"
|
|
||||||
---
|
|
||||||
# Source: tline/templates/ingress-tailscale.yaml.tpl
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
name: tline-tline-web
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/managed-by: Helm
|
|
||||||
helm.sh/chart: "tline-0.1.0"
|
|
||||||
app.kubernetes.io/component: web
|
|
||||||
annotations:
|
|
||||||
spec:
|
|
||||||
ingressClassName: tailscale
|
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- "app"
|
|
||||||
rules:
|
|
||||||
- host: "app"
|
|
||||||
http:
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
backend:
|
|
||||||
service:
|
|
||||||
name: tline-tline-web
|
|
||||||
port:
|
|
||||||
number: 3000
|
|
||||||
---
|
|
||||||
# Source: tline/templates/ingress-tailscale.yaml.tpl
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
name: tline-tline-minio
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/managed-by: Helm
|
|
||||||
helm.sh/chart: "tline-0.1.0"
|
|
||||||
app.kubernetes.io/component: minio
|
|
||||||
annotations:
|
|
||||||
spec:
|
|
||||||
ingressClassName: tailscale
|
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- "minio"
|
|
||||||
rules:
|
|
||||||
- host: "minio"
|
|
||||||
http:
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
backend:
|
|
||||||
service:
|
|
||||||
name: tline-tline-minio
|
|
||||||
port:
|
|
||||||
number: 9000
|
|
||||||
---
|
|
||||||
# Source: tline/templates/ingress-tailscale.yaml.tpl
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
name: tline-tline-minio-console
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/managed-by: Helm
|
|
||||||
helm.sh/chart: "tline-0.1.0"
|
|
||||||
app.kubernetes.io/component: minio
|
|
||||||
annotations:
|
|
||||||
spec:
|
|
||||||
ingressClassName: tailscale
|
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- "minio-console"
|
|
||||||
rules:
|
|
||||||
- host: "minio-console"
|
|
||||||
http:
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
backend:
|
|
||||||
service:
|
|
||||||
name: tline-tline-minio
|
|
||||||
port:
|
|
||||||
number: 9001
|
|
||||||
---
|
|
||||||
# Source: tline/templates/job-migrate.yaml.tpl
|
|
||||||
apiVersion: batch/v1
|
|
||||||
kind: Job
|
|
||||||
metadata:
|
|
||||||
name: tline-tline-migrate
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/managed-by: Helm
|
|
||||||
helm.sh/chart: "tline-0.1.0"
|
|
||||||
app.kubernetes.io/component: migrate
|
|
||||||
annotations:
|
|
||||||
"helm.sh/hook": pre-install,pre-upgrade
|
|
||||||
"helm.sh/hook-weight": "-10"
|
|
||||||
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
|
|
||||||
spec:
|
|
||||||
backoffLimit: 3
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: tline
|
|
||||||
app.kubernetes.io/instance: tline
|
|
||||||
app.kubernetes.io/component: migrate
|
|
||||||
spec:
|
|
||||||
restartPolicy: Never
|
|
||||||
imagePullSecrets:
|
|
||||||
- name: "tline-tline-registry"
|
|
||||||
affinity:
|
|
||||||
nodeAffinity:
|
|
||||||
requiredDuringSchedulingIgnoredDuringExecution:
|
|
||||||
nodeSelectorTerms:
|
|
||||||
- matchExpressions:
|
|
||||||
- key: node-class
|
|
||||||
operator: In
|
|
||||||
values:
|
|
||||||
- compute
|
|
||||||
containers:
|
|
||||||
- name: migrate
|
|
||||||
image: "registry.lan:5000/tline-worker:dev"
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
command:
|
|
||||||
- bun
|
|
||||||
- run
|
|
||||||
- packages/db/src/migrate.ts
|
|
||||||
envFrom:
|
|
||||||
- configMapRef:
|
|
||||||
name: tline-tline-config
|
|
||||||
env:
|
|
||||||
- name: POSTGRES_PASSWORD
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: tline-tline-secrets
|
|
||||||
key: POSTGRES_PASSWORD
|
|
||||||
|
|||||||
@@ -1,25 +1,2 @@
|
|||||||
secrets:
|
# Temporary file used during local helm rendering.
|
||||||
postgres:
|
# This file is intentionally empty in-repo; real values should not be committed.
|
||||||
password: "change-me"
|
|
||||||
minio:
|
|
||||||
accessKeyId: "minioadmin"
|
|
||||||
secretAccessKey: "minioadmin"
|
|
||||||
|
|
||||||
images:
|
|
||||||
web:
|
|
||||||
repository: registry.lan:5000/tline-web
|
|
||||||
tag: dev
|
|
||||||
worker:
|
|
||||||
repository: registry.lan:5000/tline-worker
|
|
||||||
tag: dev
|
|
||||||
|
|
||||||
global:
|
|
||||||
tailscale:
|
|
||||||
tailnetFQDN: "tailxyz.ts.net"
|
|
||||||
|
|
||||||
registrySecret:
|
|
||||||
create: true
|
|
||||||
server: "registry.lan:5000"
|
|
||||||
username: "u"
|
|
||||||
password: "p"
|
|
||||||
email: "e@example.com"
|
|
||||||
|
|||||||
681
docs/plans/2026-02-01-all-future-features.md
Normal file
681
docs/plans/2026-02-01-all-future-features.md
Normal file
@@ -0,0 +1,681 @@
|
|||||||
|
# All Future Features Implementation Plan
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Implement all items in `PLAN.md` under "Future Features (Tracked)" for the `porthole` app.
|
||||||
|
|
||||||
|
**Architecture:** Add features incrementally behind small, testable boundaries: shared-secret admin auth for writes, a normalized derived-variant model for media, a transcoding pipeline (MP4 first), tagging/albums and metadata overrides with audit logging, dedupe + moments, GPS extraction + map UI (no reverse geocoding), endpoint selection for presigned URLs, and CI-based multi-arch builds.
|
||||||
|
|
||||||
|
**Tech Stack:** Bun workspaces, Next.js API route handlers (`apps/web/app/api/**/route.ts`), Node worker + BullMQ (`apps/worker/src/jobs.ts`), Postgres migrations (`packages/db/migrations/*.sql`), MinIO (S3) clients (`packages/minio/src/index.ts`), Helm (`helm/porthole/*`).
|
||||||
|
|
||||||
|
## Preconditions / Ground Rules
|
||||||
|
|
||||||
|
- Do not mutate or delete anything under `originals/`.
|
||||||
|
- Prefer additive schema changes first; deprecate old columns after compatibility is maintained.
|
||||||
|
- Use Bun’s test runner (`bun test`) for new TypeScript tests.
|
||||||
|
- Keep Pi constraints: CPU-heavy work stays in worker; keep transcoding concurrency low.
|
||||||
|
|
||||||
|
## Phase 0: Test Harness + Repo Hygiene
|
||||||
|
|
||||||
|
### Task 0.1: Add Bun test runner scripts
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `package.json`
|
||||||
|
- Create: `apps/web/src/__tests__/smoke.test.ts`
|
||||||
|
|
||||||
|
**Step 1: Write failing test**
|
||||||
|
|
||||||
|
Create `apps/web/src/__tests__/smoke.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { test, expect } from "bun:test";
|
||||||
|
|
||||||
|
test("bun test runs", () => {
|
||||||
|
expect(1 + 1).toBe(2);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test`
|
||||||
|
Expected: FAIL (no tests configured / command missing)
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Add script to `package.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"test": "bun test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `bun test`
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add package.json apps/web/src/__tests__/smoke.test.ts
|
||||||
|
git commit -m "test: add bun test runner"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 1: Shared-Secret Admin Auth (Write Protection)
|
||||||
|
|
||||||
|
### Task 1.1: Add ADMIN_TOKEN env + helpers
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/config/src/index.ts`
|
||||||
|
- Create: `packages/config/src/adminAuth.ts`
|
||||||
|
- Test: `packages/config/src/adminAuth.test.ts`
|
||||||
|
|
||||||
|
**Step 1: Write failing test**
|
||||||
|
|
||||||
|
Create `packages/config/src/adminAuth.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { test, expect } from "bun:test";
|
||||||
|
import { isAdminRequest } from "./adminAuth";
|
||||||
|
|
||||||
|
test("isAdminRequest returns false when ADMIN_TOKEN unset", () => {
|
||||||
|
expect(isAdminRequest({ adminToken: undefined }, { headerToken: "x" })).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("isAdminRequest returns true when header token matches", () => {
|
||||||
|
expect(
|
||||||
|
isAdminRequest({ adminToken: "secret" }, { headerToken: "secret" }),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/config/src/adminAuth.test.ts`
|
||||||
|
Expected: FAIL (module missing)
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Create `packages/config/src/adminAuth.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function isAdminRequest(
|
||||||
|
env: { adminToken: string | undefined },
|
||||||
|
input: { headerToken: string | null | undefined },
|
||||||
|
) {
|
||||||
|
if (!env.adminToken) return false;
|
||||||
|
return input.headerToken === env.adminToken;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Extend `packages/config/src/index.ts` to parse `ADMIN_TOKEN` (optional) and export `getAdminToken()`.
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `bun test packages/config/src/adminAuth.test.ts`
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/config/src/index.ts packages/config/src/adminAuth.ts packages/config/src/adminAuth.test.ts
|
||||||
|
git commit -m "feat: add admin token config and auth helper"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 1.2: Enforce admin on mutation API routes
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `apps/web/app/api/imports/route.ts`
|
||||||
|
- Modify: `apps/web/app/api/imports/[id]/upload/route.ts`
|
||||||
|
- Modify: `apps/web/app/api/imports/[id]/scan-minio/route.ts`
|
||||||
|
- Test: `apps/web/src/__tests__/admin-gates-imports.test.ts`
|
||||||
|
|
||||||
|
**Step 1: Write failing test**
|
||||||
|
|
||||||
|
Create `apps/web/src/__tests__/admin-gates-imports.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { test, expect } from "bun:test";
|
||||||
|
|
||||||
|
// This test intentionally asserts the handler behavior by calling the route function.
|
||||||
|
// It will require exporting a small pure helper from each route in the implementation.
|
||||||
|
|
||||||
|
test("imports POST rejects when missing admin token", async () => {
|
||||||
|
const { handleCreateImport } = await import("../../app/api/imports/handlers");
|
||||||
|
const res = await handleCreateImport({ adminOk: false, body: {} });
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test apps/web/src/__tests__/admin-gates-imports.test.ts`
|
||||||
|
Expected: FAIL (handlers module missing)
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
- Create `apps/web/app/api/imports/handlers.ts` exporting pure functions that return `{ status, body }` for tests.
|
||||||
|
- Update `apps/web/app/api/imports/route.ts` to:
|
||||||
|
- read `X-Porthole-Admin-Token`
|
||||||
|
- compute adminOk via `@tline/config` helper
|
||||||
|
- reject with 401 `{ error: "admin_required" }` when not admin
|
||||||
|
- Repeat pattern for upload + scan routes.
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `bun test apps/web/src/__tests__/admin-gates-imports.test.ts`
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add apps/web/app/api/imports/route.ts apps/web/app/api/imports/handlers.ts \
|
||||||
|
apps/web/app/api/imports/[id]/upload/route.ts apps/web/app/api/imports/[id]/scan-minio/route.ts \
|
||||||
|
apps/web/src/__tests__/admin-gates-imports.test.ts
|
||||||
|
git commit -m "feat: require admin token for ingestion endpoints"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 2: Derived Variants Model (Thumbs/Posters/Video)
|
||||||
|
|
||||||
|
### Task 2.1: Add derived variants table + minimal writer/reader
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `packages/db/migrations/0003_asset_variants.sql`
|
||||||
|
- Modify: `apps/web/app/api/assets/[id]/url/route.ts`
|
||||||
|
- Modify: `apps/worker/src/jobs.ts`
|
||||||
|
- Test: `apps/web/src/__tests__/variant-url-404.test.ts`
|
||||||
|
|
||||||
|
**Schema (migration):**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TYPE IF NOT EXISTS asset_variant_kind AS ENUM (
|
||||||
|
'thumb',
|
||||||
|
'poster',
|
||||||
|
'video_mp4'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS asset_variants (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
asset_id uuid NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
|
||||||
|
kind asset_variant_kind NOT NULL,
|
||||||
|
size int NOT NULL,
|
||||||
|
key text NOT NULL,
|
||||||
|
mime_type text NOT NULL,
|
||||||
|
width int,
|
||||||
|
height int,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE(asset_id, kind, size)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS asset_variants_asset_id_idx ON asset_variants(asset_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 1: Write failing test**
|
||||||
|
|
||||||
|
Create `apps/web/src/__tests__/variant-url-404.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { test, expect } from "bun:test";
|
||||||
|
|
||||||
|
test("/api/assets/:id/url returns 404 when requested variant missing", async () => {
|
||||||
|
const { pickVariantKey } =
|
||||||
|
await import("../../app/api/assets/[id]/url/variant");
|
||||||
|
const key = pickVariantKey({ variants: [] }, { kind: "thumb", size: 256 });
|
||||||
|
expect(key).toBeNull();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test apps/web/src/__tests__/variant-url-404.test.ts`
|
||||||
|
Expected: FAIL (module missing)
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
- Create `apps/web/app/api/assets/[id]/url/variant.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function pickVariantKey(
|
||||||
|
input: { variants: Array<{ kind: string; size: number; key: string }> },
|
||||||
|
req: { kind: string; size: number },
|
||||||
|
) {
|
||||||
|
const v = input.variants.find(
|
||||||
|
(x) => x.kind === req.kind && x.size === req.size,
|
||||||
|
);
|
||||||
|
return v?.key ?? null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Update `apps/web/app/api/assets/[id]/url/route.ts` to support query:
|
||||||
|
- `kind=original|thumb|poster|video_mp4`
|
||||||
|
- `size=<int>` (required for non-original)
|
||||||
|
- Keep backward-compatible `variant=thumb_small|thumb_med|poster|original` for now.
|
||||||
|
|
||||||
|
- Update `apps/worker/src/jobs.ts` to insert rows into `asset_variants` when it uploads thumbs/posters.
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `bun test apps/web/src/__tests__/variant-url-404.test.ts`
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/db/migrations/0003_asset_variants.sql \
|
||||||
|
apps/web/app/api/assets/[id]/url/route.ts apps/web/app/api/assets/[id]/url/variant.ts \
|
||||||
|
apps/web/src/__tests__/variant-url-404.test.ts apps/worker/src/jobs.ts
|
||||||
|
git commit -m "feat: add asset variants table and URL selection"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2.2: Multiple thumb + poster sizes
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `apps/worker/src/jobs.ts`
|
||||||
|
- Modify: `apps/web/app/api/assets/[id]/url/route.ts`
|
||||||
|
- Test: `apps/worker/src/__tests__/variants-sizes.test.ts`
|
||||||
|
|
||||||
|
**Step 1: Write failing test**
|
||||||
|
|
||||||
|
Create `apps/worker/src/__tests__/variants-sizes.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { test, expect } from "bun:test";
|
||||||
|
import { computeImageVariantPlan } from "../variants";
|
||||||
|
|
||||||
|
test("computeImageVariantPlan includes 256 and 768 thumbs", () => {
|
||||||
|
expect(computeImageVariantPlan()).toEqual([
|
||||||
|
{ kind: "thumb", size: 256 },
|
||||||
|
{ kind: "thumb", size: 768 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test apps/worker/src/__tests__/variants-sizes.test.ts`
|
||||||
|
Expected: FAIL (module missing)
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
- Create `apps/worker/src/variants.ts` with exported `computeImageVariantPlan()` and `computeVideoPosterPlan()`.
|
||||||
|
- Refactor `apps/worker/src/jobs.ts` to use these plans and generate additional poster size(s) (e.g. 256 + 768).
|
||||||
|
- Insert each uploaded object into `asset_variants` with (kind,size,key,mime_type,width,height).
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `bun test apps/worker/src/__tests__/variants-sizes.test.ts`
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add apps/worker/src/jobs.ts apps/worker/src/variants.ts apps/worker/src/__tests__/variants-sizes.test.ts
|
||||||
|
git commit -m "feat: generate multiple thumbs and posters"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 3: Video Transcoding + Prefer-Derived Playback
|
||||||
|
|
||||||
|
### Task 3.1: Add MP4 transcode worker job
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/queue/src/index.ts`
|
||||||
|
- Modify: `apps/worker/src/jobs.ts`
|
||||||
|
- Test: `apps/worker/src/__tests__/transcode-plan.test.ts`
|
||||||
|
|
||||||
|
**Step 1: Write failing test**
|
||||||
|
|
||||||
|
Create `apps/worker/src/__tests__/transcode-plan.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { test, expect } from "bun:test";
|
||||||
|
import { shouldTranscodeToMp4 } from "../transcode";
|
||||||
|
|
||||||
|
test("transcode runs for non-mp4 videos", () => {
|
||||||
|
expect(shouldTranscodeToMp4({ mimeType: "video/x-matroska" })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("transcode skips for mp4", () => {
|
||||||
|
expect(shouldTranscodeToMp4({ mimeType: "video/mp4" })).toBe(false);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test apps/worker/src/__tests__/transcode-plan.test.ts`
|
||||||
|
Expected: FAIL
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
- Create `apps/worker/src/transcode.ts` implementing `shouldTranscodeToMp4`.
|
||||||
|
- Add BullMQ job payload + enqueue helper (e.g. `enqueueTranscodeVideoMp4({ assetId })`).
|
||||||
|
- In `handleProcessAsset` for video, enqueue mp4 transcode when needed.
|
||||||
|
- Implement ffmpeg transcode to `derived/video/${assetId}/mp4_720p.mp4` (H.264 + AAC, fast preset).
|
||||||
|
- Insert into `asset_variants` as `kind='video_mp4', size=720, mime_type='video/mp4'`.
|
||||||
|
- Keep concurrency low (1) in worker for transcodes.
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `bun test apps/worker/src/__tests__/transcode-plan.test.ts`
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/queue/src/index.ts apps/worker/src/jobs.ts apps/worker/src/transcode.ts \
|
||||||
|
apps/worker/src/__tests__/transcode-plan.test.ts
|
||||||
|
git commit -m "feat: add mp4 transcode job and variant record"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 3.2: Prefer derived in URL endpoint + viewer
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `apps/web/app/api/assets/[id]/url/route.ts`
|
||||||
|
- Modify: `apps/web/app/components/MediaPanel.tsx`
|
||||||
|
- Modify: `apps/web/app/components/Viewer.tsx`
|
||||||
|
- Test: `apps/web/src/__tests__/prefer-derived.test.ts`
|
||||||
|
|
||||||
|
**Step 1: Write failing test**
|
||||||
|
|
||||||
|
Create `apps/web/src/__tests__/prefer-derived.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { test, expect } from "bun:test";
|
||||||
|
import { pickVideoPlaybackVariant } from "../../app/lib/playback";
|
||||||
|
|
||||||
|
test("prefer mp4 derived over original", () => {
|
||||||
|
const picked = pickVideoPlaybackVariant({
|
||||||
|
originalMimeType: "video/x-matroska",
|
||||||
|
variants: [{ kind: "video_mp4", size: 720, key: "derived/video/a.mp4" }],
|
||||||
|
});
|
||||||
|
expect(picked).toEqual({ kind: "video_mp4", size: 720 });
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test apps/web/src/__tests__/prefer-derived.test.ts`
|
||||||
|
Expected: FAIL
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
- Create `apps/web/app/lib/playback.ts` implementing deterministic selection.
|
||||||
|
- Update viewer to:
|
||||||
|
- ask server for `kind=video_mp4&size=720` first
|
||||||
|
- fall back to `original`
|
||||||
|
- Keep existing poster behavior.
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `bun test apps/web/src/__tests__/prefer-derived.test.ts`
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add apps/web/app/api/assets/[id]/url/route.ts apps/web/app/lib/playback.ts \
|
||||||
|
apps/web/app/components/MediaPanel.tsx apps/web/app/components/Viewer.tsx \
|
||||||
|
apps/web/src/__tests__/prefer-derived.test.ts
|
||||||
|
git commit -m "feat: prefer derived mp4 playback with fallback"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 4: Tags + Albums
|
||||||
|
|
||||||
|
### Task 4.1: Schema for tags/albums + audit log
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `packages/db/migrations/0004_tags_albums_audit.sql`
|
||||||
|
|
||||||
|
**Migration (example):**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE IF NOT EXISTS tags (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name text NOT NULL UNIQUE,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS asset_tags (
|
||||||
|
asset_id uuid NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
|
||||||
|
tag_id uuid NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY(asset_id, tag_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS albums (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name text NOT NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS album_assets (
|
||||||
|
album_id uuid NOT NULL REFERENCES albums(id) ON DELETE CASCADE,
|
||||||
|
asset_id uuid NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
|
||||||
|
ord int,
|
||||||
|
PRIMARY KEY(album_id, asset_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS audit_log (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
actor text NOT NULL,
|
||||||
|
action text NOT NULL,
|
||||||
|
entity_type text NOT NULL,
|
||||||
|
entity_id uuid,
|
||||||
|
payload jsonb,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verification:** run migrator (k8s migrate job or local script) and ensure no SQL errors.
|
||||||
|
|
||||||
|
**Commit:** `git commit -m "feat: add tags, albums, and audit log tables"`
|
||||||
|
|
||||||
|
### Task 4.2: Admin API for tags and albums
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `apps/web/app/api/tags/route.ts`
|
||||||
|
- Create: `apps/web/app/api/albums/route.ts`
|
||||||
|
- Create: `apps/web/app/api/albums/[id]/assets/route.ts`
|
||||||
|
- Test: `apps/web/src/__tests__/tags-admin-auth.test.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
- RED: test that POST without admin returns 401
|
||||||
|
- GREEN: implement CRUD (minimal: list + create; album add/remove assets)
|
||||||
|
- REFACTOR: write audit_log rows on each mutation
|
||||||
|
|
||||||
|
**Commit:** `feat: add admin tags and albums APIs`
|
||||||
|
|
||||||
|
### Task 4.3: UI wiring for tags/albums
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `apps/web/app/admin/page.tsx`
|
||||||
|
- Modify: `apps/web/app/components/MediaPanel.tsx`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
- Add minimal admin form to set admin token in browser (sessionStorage) and to create/list tags and albums.
|
||||||
|
- Add UI on asset detail to assign tags, and to add asset to album.
|
||||||
|
- Keep UX resilient (errors render inline, don’t crash).
|
||||||
|
|
||||||
|
**Commit:** `feat: add tags/albums UI`
|
||||||
|
|
||||||
|
## Phase 5: Metadata Overrides + Timeline Uses Overrides
|
||||||
|
|
||||||
|
### Task 5.1: Override table + API
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `packages/db/migrations/0005_asset_overrides.sql`
|
||||||
|
- Create: `apps/web/app/api/assets/[id]/override-capture-ts/route.ts`
|
||||||
|
- Modify: `apps/web/app/api/tree/route.ts`
|
||||||
|
- Modify: `apps/web/app/api/assets/route.ts`
|
||||||
|
|
||||||
|
**Migration:** table `asset_overrides(asset_id PK, capture_ts_utc_override timestamptz, capture_offset_minutes_override int, created_at...)`.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
- RED: test route rejects without admin
|
||||||
|
- GREEN: implement POST to set override and insert audit_log
|
||||||
|
- GREEN: update aggregation queries to use `COALESCE(overrides.capture_ts_utc_override, assets.capture_ts_utc)`
|
||||||
|
|
||||||
|
**Commit:** `feat: add capture time overrides and apply in queries`
|
||||||
|
|
||||||
|
### Task 5.2: UI for capture-time override
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `apps/web/app/components/Viewer.tsx`
|
||||||
|
- Modify: `apps/web/app/components/MediaPanel.tsx`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
- Add form to set ISO timestamp override and submit to API.
|
||||||
|
- Display current effective timestamp and base timestamp.
|
||||||
|
|
||||||
|
**Commit:** `feat: add UI for capture time override`
|
||||||
|
|
||||||
|
## Phase 6: GPS Extraction + Map UI (No Reverse Geocode)
|
||||||
|
|
||||||
|
### Task 6.1: Add gps fields + extraction
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `packages/db/migrations/0006_assets_gps.sql`
|
||||||
|
- Modify: `apps/worker/src/jobs.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
- Add columns `gps_lat double precision`, `gps_lon double precision` (nullable)
|
||||||
|
- Parse ExifTool GPS fields for images (and where available for videos) and store them.
|
||||||
|
|
||||||
|
**Commit:** `feat: extract and store GPS coords`
|
||||||
|
|
||||||
|
### Task 6.2: Map UI
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `apps/web/app/map/page.tsx`
|
||||||
|
- Modify: `apps/web/app/page.tsx`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
- Show a simple map view with markers for assets that have GPS.
|
||||||
|
- If tiles unavailable, show a clear fallback message.
|
||||||
|
|
||||||
|
**Commit:** `feat: add map page for GPS assets`
|
||||||
|
|
||||||
|
## Phase 7: Dedupe by Hash + Moments
|
||||||
|
|
||||||
|
### Task 7.1: Hash table + compute sha256
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `packages/db/migrations/0007_asset_hashes.sql`
|
||||||
|
- Modify: `apps/worker/src/jobs.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
- During download to temp file, compute sha256 and store it.
|
||||||
|
- Add unique index on `(bucket, sha256)` optionally (careful for partial/unknown).
|
||||||
|
|
||||||
|
**Commit:** `feat: compute asset sha256 for dedupe`
|
||||||
|
|
||||||
|
### Task 7.2: Dedupe detection + API
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `apps/web/app/api/assets/[id]/dupes/route.ts`
|
||||||
|
- Modify: `apps/web/app/components/MediaPanel.tsx`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
- Endpoint returns assets with same sha256.
|
||||||
|
- UI indicates duplicates.
|
||||||
|
|
||||||
|
**Commit:** `feat: expose and display duplicates`
|
||||||
|
|
||||||
|
### Task 7.3: Moments clustering
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `apps/web/app/api/moments/route.ts`
|
||||||
|
- Create: `apps/web/app/lib/moments.ts`
|
||||||
|
- Test: `apps/web/src/__tests__/moments.test.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
- RED: test that assets within 30 minutes cluster together
|
||||||
|
- GREEN: implement clustering
|
||||||
|
- Wire UI to show moments as sub-groups
|
||||||
|
|
||||||
|
**Commit:** `feat: add day moments clustering`
|
||||||
|
|
||||||
|
## Phase 8: Presign Endpoint Selection (LAN vs Tailnet)
|
||||||
|
|
||||||
|
### Task 8.1: Add endpoint mode to config and presign
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/minio/src/index.ts`
|
||||||
|
- Modify: `packages/config/src/index.ts`
|
||||||
|
- Modify: `apps/web/app/api/assets/[id]/url/route.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
- Add env for `MINIO_PUBLIC_ENDPOINT_LAN` and `MINIO_ENDPOINT_MODE=tailnet|lan|auto`.
|
||||||
|
- If `endpoint=lan|tailnet` query param is provided, force that.
|
||||||
|
- In `auto`, use tailnet as safe default.
|
||||||
|
|
||||||
|
**Commit:** `feat: support lan/tailnet endpoint selection for presigned URLs`
|
||||||
|
|
||||||
|
## Phase 9: Storage Policies (Derived Lifecycle) + CI Builds
|
||||||
|
|
||||||
|
### Task 9.1: Optional MinIO lifecycle policy job
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `helm/porthole/values.yaml`
|
||||||
|
- Modify: `helm/porthole/templates/job-ensure-bucket.yaml.tpl`
|
||||||
|
- Create: `helm/porthole/templates/job-apply-lifecycle.yaml.tpl`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
- Add optional Job to apply lifecycle rules for prefixes `thumbs/` and `derived/` (expire after N days) without touching `originals/`.
|
||||||
|
|
||||||
|
**Commit:** `feat: add optional lifecycle policy job`
|
||||||
|
|
||||||
|
### Task 9.2: Add CI pipeline for multi-arch builds
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `.gitea/workflows/build-images.yml` (or alternative supported by your CI)
|
||||||
|
- Modify: `README.md`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
- Build and push multi-arch images for `apps/web` and `apps/worker`.
|
||||||
|
- Run: `bun run typecheck`.
|
||||||
|
- Run: `bash run_tests.sh` (Go tests) to keep repo green.
|
||||||
|
|
||||||
|
**Commit:** `ci: build and push multi-arch images`
|
||||||
|
|
||||||
|
## Verification Checklist (Per Phase)
|
||||||
|
|
||||||
|
- `bun test`
|
||||||
|
- `bun run typecheck`
|
||||||
|
- `bash run_tests.sh`
|
||||||
|
- Helm template renders: `helm template porthole helm/porthole -f your-values.yaml --namespace porthole`
|
||||||
Reference in New Issue
Block a user