Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd2b8e6241 | |||
| 4328bbaeda | |||
| bf8e98dd49 | |||
| 2ebfa4caaf |
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.git
|
||||||
|
.gitea
|
||||||
|
build
|
||||||
|
desktop
|
||||||
|
frontend/node_modules
|
||||||
|
frontend/dist
|
||||||
|
server/webdist/dist
|
||||||
|
notes.zip
|
||||||
|
*.md
|
||||||
|
.env.docker
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,4 +9,5 @@ desktop/build/bin/
|
|||||||
*.db-wal
|
*.db-wal
|
||||||
*.db-shm
|
*.db-shm
|
||||||
notes.zip
|
notes.zip
|
||||||
|
.env.docker
|
||||||
!server/webdist/dist/.gitkeep
|
!server/webdist/dist/.gitkeep
|
||||||
|
|||||||
40
Dockerfile
Normal file
40
Dockerfile
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Build the frontend bundle (vite), then the static tefterd binary that
|
||||||
|
# embeds it, then ship binary-only. VERSION flows in from compose
|
||||||
|
# (${TEFTER_VERSION}) so `tefterd version` matches the git describe.
|
||||||
|
|
||||||
|
# Fully-qualified images (podman enforces short-name resolution non-interactively).
|
||||||
|
FROM docker.io/library/node:22-alpine AS frontend
|
||||||
|
WORKDIR /src/frontend
|
||||||
|
COPY frontend/package.json frontend/package-lock.json ./
|
||||||
|
RUN npm ci --no-audit --no-fund
|
||||||
|
COPY frontend/ ./
|
||||||
|
ARG VERSION=dev
|
||||||
|
RUN TEFTER_VERSION=$VERSION npx vite build
|
||||||
|
|
||||||
|
FROM docker.io/library/golang:1.25-alpine AS build
|
||||||
|
# git enables the GOPROXY "direct" fallback (fetch modules from their VCS).
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
# Overridable for hosts where proxy.golang.org (Google) is unreachable —
|
||||||
|
# set e.g. GOPROXY=https://goproxy.io,direct in .env.docker. Module
|
||||||
|
# integrity is still verified against the committed go.sum either way.
|
||||||
|
ARG GOPROXY=https://proxy.golang.org,direct
|
||||||
|
ENV GOPROXY=$GOPROXY
|
||||||
|
WORKDIR /src/server
|
||||||
|
COPY server/go.mod server/go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY server/ ./
|
||||||
|
# go:embed cannot reach outside the module dir, so the bundle is copied in
|
||||||
|
# (same as the Makefile does).
|
||||||
|
COPY --from=frontend /src/frontend/dist ./webdist/dist
|
||||||
|
ARG VERSION=dev
|
||||||
|
RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.Version=$VERSION" -o /tefterd .
|
||||||
|
|
||||||
|
FROM docker.io/library/alpine:3.22
|
||||||
|
RUN adduser -D -H tefter && mkdir -p /data && chown tefter /data
|
||||||
|
COPY --from=build /tefterd /usr/local/bin/tefterd
|
||||||
|
USER tefter
|
||||||
|
ENV TEFTER_DB=/data/tefter.db
|
||||||
|
# The auth token is generated and printed to the logs on first start.
|
||||||
|
EXPOSE 8420
|
||||||
|
VOLUME /data
|
||||||
|
CMD ["tefterd"]
|
||||||
32
deploy/redeploy-tefter.sh
Executable file
32
deploy/redeploy-tefter.sh
Executable file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
APP_DIR=$HOME/tefter
|
||||||
|
SERVICE=tefter.service
|
||||||
|
|
||||||
|
cd "$APP_DIR"
|
||||||
|
|
||||||
|
# Host-specific settings (WEB_PORT, GOPROXY, ...) live in .env.docker next to
|
||||||
|
# the compose file; export them so compose interpolation sees them.
|
||||||
|
set -a; [ -f .env.docker ] && . ./.env.docker; set +a
|
||||||
|
WEB_PORT=${WEB_PORT:-8420}
|
||||||
|
|
||||||
|
echo "==> Pulling latest code..."
|
||||||
|
git pull
|
||||||
|
|
||||||
|
echo "==> Rebuilding container..."
|
||||||
|
TEFTER_VERSION=$(git describe --tags --always --dirty) podman-compose build
|
||||||
|
|
||||||
|
echo "==> Restarting service..."
|
||||||
|
systemctl --user restart $SERVICE
|
||||||
|
|
||||||
|
echo "==> Waiting for boot..."
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
if curl -s -o /dev/null -w "" http://localhost:$WEB_PORT/; then
|
||||||
|
echo "==> Deploy successful! App is running on port $WEB_PORT."
|
||||||
|
podman-compose logs --tail 5 tefter 2>&1
|
||||||
|
else
|
||||||
|
echo "==> WARNING: App may not be ready yet. Check logs:"
|
||||||
|
podman-compose logs --tail 20 tefter 2>&1
|
||||||
|
fi
|
||||||
20
deploy/tefter.service
Normal file
20
deploy/tefter.service
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# User unit — install to ~/.config/systemd/user/tefter.service, then:
|
||||||
|
# systemctl --user daemon-reload && systemctl --user enable --now tefter.service
|
||||||
|
# Requires lingering (loginctl enable-linger) so it survives logout.
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Tefter notes server (podman-compose)
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=%h/tefter
|
||||||
|
EnvironmentFile=%h/tefter/.env.docker
|
||||||
|
ExecStart=/home/hamo/.local/bin/podman-compose up
|
||||||
|
ExecStop=/home/hamo/.local/bin/podman-compose down
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
21
docker-compose.yml
Normal file
21
docker-compose.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
services:
|
||||||
|
tefter:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
VERSION: ${TEFTER_VERSION:-dev}
|
||||||
|
GOPROXY: ${GOPROXY:-https://proxy.golang.org,direct}
|
||||||
|
environment:
|
||||||
|
TEFTER_DB: /data/tefter.db
|
||||||
|
volumes:
|
||||||
|
# ":Z" relabels for SELinux (required on the podman/SELinux server; a
|
||||||
|
# harmless no-op on non-SELinux Docker hosts).
|
||||||
|
- data:/data:Z
|
||||||
|
security_opt:
|
||||||
|
- label:disable
|
||||||
|
ports:
|
||||||
|
- "${WEB_PORT:-8420}:8420"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
@@ -106,39 +106,52 @@
|
|||||||
|
|
||||||
// Swap document when a different note is opened; apply external (sync) changes
|
// Swap document when a different note is opened; apply external (sync) changes
|
||||||
// without clobbering in-flight typing.
|
// without clobbering in-flight typing.
|
||||||
$: if (view && note) {
|
function applyNote(n: Note) {
|
||||||
if (note.id !== currentId) {
|
if (!view || !n) return;
|
||||||
|
if (n.id !== currentId) {
|
||||||
if (saveTimer) {
|
if (saveTimer) {
|
||||||
clearTimeout(saveTimer);
|
clearTimeout(saveTimer);
|
||||||
saveTimer = null;
|
saveTimer = null;
|
||||||
flushSave(currentId, view.state.doc.toString());
|
flushSave(currentId, view.state.doc.toString());
|
||||||
}
|
}
|
||||||
currentId = note.id;
|
currentId = n.id;
|
||||||
lastKnown = note.content;
|
lastKnown = n.content;
|
||||||
view.setState(makeState(note.content));
|
view.setState(makeState(n.content));
|
||||||
} else if (note.content !== lastKnown) {
|
} else if (n.content !== lastKnown) {
|
||||||
// Store changed underneath us (sync pull / conflict replacement) — not an
|
// Store changed underneath us (sync pull / conflict replacement) — not an
|
||||||
// echo of our own autosave. Replace the doc.
|
// echo of our own autosave. Replace the doc.
|
||||||
lastKnown = note.content;
|
lastKnown = n.content;
|
||||||
if (saveTimer) {
|
if (saveTimer) {
|
||||||
clearTimeout(saveTimer);
|
clearTimeout(saveTimer);
|
||||||
saveTimer = null;
|
saveTimer = null;
|
||||||
}
|
}
|
||||||
if (note.content !== view.state.doc.toString()) {
|
if (n.content !== view.state.doc.toString()) {
|
||||||
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: note.content } });
|
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: n.content } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastQuery = '';
|
let lastQuery = '';
|
||||||
$: if (view && query !== lastQuery) {
|
function applyQuery(q: string) {
|
||||||
lastQuery = query;
|
if (!view || q === lastQuery) return;
|
||||||
view.dispatch({ effects: searchMark.reconfigure(searchExtension(query)) });
|
lastQuery = q;
|
||||||
|
view.dispatch({ effects: searchMark.reconfigure(searchExtension(q)) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deliberately `$: fn(prop)` instead of inlining the logic in a reactive
|
||||||
|
// block: these must re-run ONLY when the prop itself changes. Svelte tracks
|
||||||
|
// every variable a `$:` statement mentions (including writes like
|
||||||
|
// `saveTimer = null` deep in the autosave path), and a re-run triggered by
|
||||||
|
// our own bookkeeping sees a stale `note`, misreads the autosave echo as an
|
||||||
|
// external sync change, and replaces the doc mid-typing — which on Android
|
||||||
|
// aborts IME composition and throws the cursor to the top of the note.
|
||||||
|
$: applyNote(note);
|
||||||
|
$: applyQuery(query);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
currentId = note.id;
|
currentId = note.id;
|
||||||
lastKnown = note.content;
|
lastKnown = note.content;
|
||||||
|
lastQuery = query; // makeState below bakes in the current query
|
||||||
view = new EditorView({ state: makeState(note.content), parent: host });
|
view = new EditorView({ state: makeState(note.content), parent: host });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user