vela 0.10.4 → 0.10.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vela",
3
- "version": "0.10.4",
3
+ "version": "0.10.6",
4
4
  "type": "module",
5
5
  "description": "A CLI for creating and updating SvelteKit projects",
6
6
  "license": "MIT",
@@ -28,3 +28,5 @@ vite.config.ts.timestamp-*
28
28
  !/data/seeds
29
29
  !/data/hooks
30
30
 
31
+ # Backups
32
+ /backups
@@ -35,7 +35,7 @@
35
35
  "supertest": "^7.2.2",
36
36
  "svelte": "^5.56.10",
37
37
  "svelte-check": "^4.6.0",
38
- "svelte-meta-tags": "^4.6.0",
38
+ "svelte-meta-tags": "^5.0.2",
39
39
  "svelte-sonner": "^1.1.0",
40
40
  "sveltekit-flash-message": "^2.4.6",
41
41
  "sveltekit-superforms": "^2.30.2",
@@ -11,7 +11,14 @@ export default defineConfig({
11
11
  runes: ({ filename }) =>
12
12
  filename.split(/[/\\]/).includes('node_modules') ? undefined : true
13
13
  },
14
- adapter: adapter()
14
+ adapter: adapter(),
15
+ // Prerendering has no request to take an origin from, so without this
16
+ // every canonical link and `og:url` on a prerendered page would be built
17
+ // from SvelteKit's placeholder host. `vela build` sets it from the domain
18
+ // the target is deployed on; unset, SvelteKit's default stands.
19
+ ...(process.env.VELA_ORIGIN
20
+ ? { prerender: { origin: process.env.VELA_ORIGIN } }
21
+ : {})
15
22
  })
16
23
  ]
17
24
  });
@@ -63,8 +63,24 @@ PB_UNIT=$(unit_pb "$INSTANCE")
63
63
  mkdir -p "$APP"/{releases,shared,bin,deps} "$APP/shared/pb_data"
64
64
  mkdir -p "$ETC"
65
65
  chmod 0700 "$ETC"
66
- chown -R "$VELA_USER:$VELA_USER" "$APP"
67
- chown "$VELA_USER:$VELA_USER" "$RELEASE_DIR"
66
+
67
+ # Only the directories just created need their ownership set, and only at the
68
+ # top level: everything below pb_data is written by PocketBase as $VELA_USER
69
+ # already. Recursing here would walk every uploaded file on every deploy, which
70
+ # makes deploy time grow with the size of the app's storage forever.
71
+ chown "$VELA_USER:$VELA_USER" \
72
+ "$APP" "$APP/releases" "$APP/shared" "$APP/shared/pb_data" "$APP/bin" "$APP/deps"
73
+ # The release is the exception - rsync uploaded it as whoever we ssh'd in as.
74
+ chown -R "$VELA_USER:$VELA_USER" "$RELEASE_DIR"
75
+
76
+ # A pb_data the app cannot write is a dead instance, and it fails as an opaque
77
+ # 500 rather than anything that names a cause. This is what the blanket recurse
78
+ # above used to paper over; checking costs one stat, repairing costs a walk that
79
+ # now happens only when something is actually wrong.
80
+ if ! runuser -u "$VELA_USER" -- test -w "$APP/shared/pb_data"; then
81
+ log "repairing ownership under shared/"
82
+ chown -R "$VELA_USER:$VELA_USER" "$APP/shared"
83
+ fi
68
84
 
69
85
  # ---------------------------------------------------------------- ports & env
70
86
 
@@ -228,30 +244,13 @@ if [ "$BACKEND" = "1" ]; then
228
244
  SU_CREATED=1
229
245
  fi
230
246
 
231
- # The password reaches PocketBase as an argument, so the upsert runs only on
232
- # the deploys that need it rather than on every one.
233
- SU_STAMP="$ETC/.superuser"
234
- SU_FINGERPRINT=$(printf '%s\n%s' "$SU_EMAIL" "$SU_PASSWORD" | sha256sum | cut -d' ' -f1)
235
- if [ "$SU_CREATED" = "1" ] || [ "$(cat "$SU_STAMP" 2>/dev/null || true)" != "$SU_FINGERPRINT" ]; then
236
- if [ "$SU_CREATED" = "1" ]; then
237
- log "creating the PocketBase superuser"
238
- else
239
- log "updating the PocketBase superuser"
240
- fi
241
- runuser -u "$VELA_USER" -- "$APP/bin/pocketbase" \
242
- --dir "$APP/shared/pb_data" \
243
- superuser upsert "$SU_EMAIL" "$SU_PASSWORD" >&2 \
244
- || die "could not create the PocketBase superuser"
245
-
246
- # Recorded only once the database holds the account, so a failure above
247
- # leaves nothing behind and the next deploy simply tries again.
248
- if [ "$SU_CREATED" = "1" ]; then
249
- env_file_append "$ETC/env" POCKETBASE_SUPERUSER_EMAIL "$SU_EMAIL"
250
- env_file_append "$ETC/env" POCKETBASE_SUPERUSER_PASSWORD "$SU_PASSWORD"
251
- fi
252
- printf '%s\n' "$SU_FINGERPRINT" > "$SU_STAMP"
253
- chmod 0600 "$SU_STAMP"
254
- chown root:root "$SU_STAMP"
247
+ reconcile_superuser "$APP" "$ETC" "$SU_EMAIL" "$SU_PASSWORD"
248
+
249
+ # Written only once the database holds the account, so a failure above leaves
250
+ # nothing behind and the next deploy simply tries again.
251
+ if [ "$SU_CREATED" = "1" ]; then
252
+ env_file_append "$ETC/env" POCKETBASE_SUPERUSER_EMAIL "$SU_EMAIL"
253
+ env_file_append "$ETC/env" POCKETBASE_SUPERUSER_PASSWORD "$SU_PASSWORD"
255
254
  fi
256
255
  fi
257
256
 
@@ -274,6 +273,25 @@ if [ "$BACKEND" = "1" ]; then
274
273
  systemctl restart "$PB_UNIT"
275
274
  wait_for_http "http://127.0.0.1:$PB_PORT/api/health" 60 0.5 \
276
275
  || die "PocketBase did not become healthy - journalctl -u $PB_UNIT"
276
+
277
+ # Answering on the port is not the same as agreeing with $ETC/env, and a
278
+ # disagreement shows up as every render 401ing rather than as a failed start.
279
+ #
280
+ # The fingerprint above cannot see a database that was replaced underneath
281
+ # an env file that did not change, so a first failure is repaired rather than
282
+ # reported: force the upsert and ask again. Only a second failure is real.
283
+ if ! assert_superuser_auth "$PB_PORT" "$SU_EMAIL" "$SU_PASSWORD"; then
284
+ log "the database disagrees with this instance's credentials - repairing"
285
+ # Stopped for the upsert: it opens the same SQLite file directly, and a
286
+ # running PocketBase is one writer too many to reason about.
287
+ systemctl stop "$PB_UNIT" >/dev/null 2>&1 || true
288
+ reconcile_superuser "$APP" "$ETC" "$SU_EMAIL" "$SU_PASSWORD" --force
289
+ systemctl start "$PB_UNIT"
290
+ wait_for_http "http://127.0.0.1:$PB_PORT/api/health" 60 0.5 \
291
+ || die "PocketBase did not come back after the credential repair"
292
+ assert_superuser_auth "$PB_PORT" "$SU_EMAIL" "$SU_PASSWORD" \
293
+ || die "PocketBase does not accept this instance's superuser credentials"
294
+ fi
277
295
  fi
278
296
 
279
297
  log "starting app on 127.0.0.1:$WEB_PORT"
@@ -19,6 +19,14 @@ require_provisioned() {
19
19
  [ -f "$VELA_ETC/provisioned" ] || die "server is not provisioned - run 'vela provision' first"
20
20
  }
21
21
 
22
+ # Refuse on a frontend-only instance. Absent state is treated as having one, so
23
+ # this only ever fires on an instance that was deployed with --backend 0.
24
+ require_backend() {
25
+ local instance=$1
26
+ [ "$(state_get "$instance" backend 2>/dev/null || echo true)" = "true" ] \
27
+ || die "$instance has no database - there is nothing to back up or restore"
28
+ }
29
+
22
30
  # Read a top-level key out of an instance's state file.
23
31
  state_get() {
24
32
  local instance=$1 key=$2 file
@@ -164,3 +172,59 @@ env_file_append() {
164
172
  random_secret() {
165
173
  head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n'
166
174
  }
175
+
176
+ # Make the instance's database agree with the credentials in its env file.
177
+ #
178
+ # The app authenticates to its own PocketBase as a superuser on every render, so
179
+ # the two have to match or every request answers 401. `$ETC/env` is the source of
180
+ # truth: this pushes whatever it holds into the database and records a
181
+ # fingerprint of it, which is what lets an ordinary deploy skip the upsert when
182
+ # nothing has moved. A caller that replaced the database underneath the env file
183
+ # passes --force, because the fingerprint alone cannot see that.
184
+ #
185
+ # usage: reconcile_superuser <app_dir> <etc_dir> <email> <password> [--force]
186
+ reconcile_superuser() {
187
+ local app=$1 etc=$2 email=$3 password=$4 force=${5:-}
188
+ local stamp="$etc/.superuser" fingerprint
189
+ fingerprint=$(printf '%s\n%s' "$email" "$password" | sha256sum | cut -d' ' -f1)
190
+
191
+ if [ "$force" != "--force" ] && [ "$(cat "$stamp" 2>/dev/null || true)" = "$fingerprint" ]; then
192
+ return 0
193
+ fi
194
+
195
+ # The stamp is written only after a successful upsert, so its absence is what
196
+ # distinguishes a first account from a changed one.
197
+ if [ -f "$stamp" ]; then
198
+ log "updating the PocketBase superuser"
199
+ else
200
+ log "creating the PocketBase superuser"
201
+ fi
202
+
203
+ # The password reaches PocketBase as an argument, which is why this runs only
204
+ # on the deploys that need it rather than on every one.
205
+ runuser -u "$VELA_USER" -- "$app/bin/pocketbase" \
206
+ --dir "$app/shared/pb_data" \
207
+ superuser upsert "$email" "$password" >&2 \
208
+ || die "could not write the PocketBase superuser"
209
+
210
+ # Recorded only once the database holds the account, so a failure above
211
+ # leaves nothing behind and the next deploy simply tries again.
212
+ printf '%s\n' "$fingerprint" > "$stamp"
213
+ chmod 0600 "$stamp"
214
+ chown root:root "$stamp"
215
+ }
216
+
217
+ # Prove the app's own credentials actually sign in to its database.
218
+ #
219
+ # `wait_for_http` accepts 4xx as healthy, so a PocketBase whose database no
220
+ # longer matches `$ETC/env` sails through the health gate and then answers every
221
+ # render with a 401. Only an actual login catches that. The password travels
222
+ # through the environment rather than argv, so it never appears in `ps`.
223
+ assert_superuser_auth() {
224
+ local port=$1 email=$2 password=$3
225
+ VELA_SU_EMAIL=$email VELA_SU_PASSWORD=$password \
226
+ jq -nc '{identity: env.VELA_SU_EMAIL, password: env.VELA_SU_PASSWORD}' \
227
+ | curl -fsS -o /dev/null --max-time 10 \
228
+ -X POST -H 'content-type: application/json' --data-binary @- \
229
+ "http://127.0.0.1:$port/api/collections/_superusers/auth-with-password"
230
+ }
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Replace one instance's database and uploads from a backup archive.
4
+ #
5
+ # This is the offline counterpart of PocketBase's own restore endpoint, which is
6
+ # not usable here for two reasons: it answers 204 and then does the work in a
7
+ # detached goroutine, so a failure is invisible to whoever asked for it; and it
8
+ # leaves the database holding whatever superuser the archive captured, while
9
+ # /etc/vela/apps/<instance>/env still holds the one the app signs in with on
10
+ # every render. Reconciling those is the whole reason this script exists.
11
+ #
12
+ # The old pb_data is renamed, never deleted. It is the only undo.
13
+ #
14
+ # usage: restore.sh <instance> --archive <path> [options]
15
+ set -Eeuo pipefail
16
+
17
+ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
18
+ # shellcheck source=lib.sh
19
+ . "$SCRIPT_DIR/lib.sh"
20
+
21
+ require_provisioned
22
+ [ "$(id -u)" -eq 0 ] || die "restore must run as root"
23
+
24
+ INSTANCE=${1:-}; shift || true
25
+ [ -n "$INSTANCE" ] || die "usage: restore.sh <instance> --archive <path> [options]"
26
+
27
+ ARCHIVE=""
28
+ MIGRATE=1
29
+ KEEP_PREVIOUS=1
30
+ CLEANUP_ARCHIVE=0
31
+ HEALTH_PATH=""
32
+
33
+ while [ $# -gt 0 ]; do
34
+ case "$1" in
35
+ --archive) ARCHIVE=$2; shift 2 ;;
36
+ --no-migrate) MIGRATE=0; shift ;;
37
+ --keep-previous) KEEP_PREVIOUS=$2; shift 2 ;;
38
+ --cleanup-archive) CLEANUP_ARCHIVE=1; shift ;;
39
+ --health-path) HEALTH_PATH=$2; shift 2 ;;
40
+ *) die "unknown argument: $1" ;;
41
+ esac
42
+ done
43
+
44
+ [ -n "$ARCHIVE" ] || die "--archive is required"
45
+
46
+ # Commands that drop to the app user inherit this working directory, and the
47
+ # directory the CLI happened to invoke from is usually one it cannot stat.
48
+ cd "$VELA_ROOT"
49
+
50
+ require_backend "$INSTANCE"
51
+
52
+ APP=$(app_dir "$INSTANCE")
53
+ ETC=$(etc_dir "$INSTANCE")
54
+ PB_DATA="$APP/shared/pb_data"
55
+ WEB_UNIT=$(unit_web "$INSTANCE")
56
+ PB_UNIT=$(unit_pb "$INSTANCE")
57
+ STAMP=$(date -u +%Y%m%dT%H%M%SZ)
58
+ PREVIOUS_DIR="$APP/shared/pb_data.pre-restore-$STAMP"
59
+
60
+ [ -d "$APP" ] || die "$INSTANCE is not deployed on this server"
61
+ [ -f "$ARCHIVE" ] || die "no archive at $ARCHIVE"
62
+
63
+ [ -n "$HEALTH_PATH" ] || HEALTH_PATH=$(state_get "$INSTANCE" healthCheckPath 2>/dev/null || echo /)
64
+
65
+ PORTS=$(allocate_ports "$INSTANCE")
66
+ WEB_PORT=$(printf '%s' "$PORTS" | jq -r .web)
67
+ PB_PORT=$(printf '%s' "$PORTS" | jq -r .pb)
68
+
69
+ # Two restores at once would each move the other's pb_data aside.
70
+ exec 9>"$APP/.restore.lock"
71
+ flock -n 9 || die "another restore is already running for $INSTANCE"
72
+
73
+ # ---------------------------------------------------------------- preflight
74
+ #
75
+ # Everything that can be checked before a service stops is checked before a
76
+ # service stops. A restore that fails here has changed nothing at all.
77
+
78
+ log "checking the archive"
79
+ unzip -tq "$ARCHIVE" >/dev/null 2>&1 || die "$ARCHIVE is not a readable zip archive"
80
+
81
+ # Restoring from a key means reading a file out of pb_data/backups - the very
82
+ # directory about to be moved aside. Copy it clear first, or the unpack below
83
+ # would be reading from a path that no longer exists.
84
+ case "$ARCHIVE" in
85
+ "$PB_DATA"/*)
86
+ mkdir -p "$APP/shared/.restore"
87
+ cp -a "$ARCHIVE" "$APP/shared/.restore/"
88
+ ARCHIVE="$APP/shared/.restore/$(basename "$ARCHIVE")"
89
+ CLEANUP_ARCHIVE=1
90
+ ;;
91
+ esac
92
+ unzip -l "$ARCHIVE" 2>/dev/null | grep -qE '[[:space:]]data\.db$' \
93
+ || die "$ARCHIVE has no data.db - it is not a PocketBase backup"
94
+
95
+ # The credentials have to exist before anything moves: without them the restored
96
+ # database cannot be reconciled, and the app would 401 on every render.
97
+ SU_EMAIL=$(env_file_get "$ETC/env" POCKETBASE_SUPERUSER_EMAIL || true)
98
+ SU_PASSWORD=$(env_file_get "$ETC/env" POCKETBASE_SUPERUSER_PASSWORD || true)
99
+ [ -n "$SU_EMAIL" ] && [ -n "$SU_PASSWORD" ] \
100
+ || die "$INSTANCE has no superuser credentials in its environment - deploy it first"
101
+
102
+ # The archive unpacks beside the directory it replaces, and the old copy is kept
103
+ # rather than deleted, so its space is never reclaimed. What has to be free is
104
+ # therefore the whole unpacked archive - plus the uploads again, if they have to
105
+ # be carried across because the archive was taken while S3 was on.
106
+ ARCHIVE_KB=$(unzip -Zt "$ARCHIVE" | awk 'NR == 1 {print int($3 / 1024) + 1}')
107
+ NEEDED_KB=$((ARCHIVE_KB + ARCHIVE_KB / 10))
108
+ if ! unzip -l "$ARCHIVE" 2>/dev/null | grep -qE '[[:space:]]storage/'; then
109
+ NEEDED_KB=$((NEEDED_KB + $(du -sk "$PB_DATA/storage" 2>/dev/null | cut -f1 || echo 0)))
110
+ fi
111
+ AVAILABLE_KB=$(df -Pk "$APP" | awk 'NR == 2 {print $4}')
112
+ if [ "$AVAILABLE_KB" -lt "$NEEDED_KB" ]; then
113
+ die "not enough disk space: the restore needs $((NEEDED_KB / 1024))M and $((AVAILABLE_KB / 1024))M is free"
114
+ fi
115
+
116
+ # ---------------------------------------------------------------- transaction
117
+
118
+ SWAPPED=0
119
+ RESTORED=0
120
+ STAGE="$APP/shared/.restore-stage.$$"
121
+
122
+ revert() {
123
+ log "restore failed - putting the previous database back"
124
+ if [ "$SWAPPED" = "1" ]; then
125
+ # Never deleted: a half-restored database is still evidence.
126
+ if [ -d "$PB_DATA" ]; then mv -Tf "$PB_DATA" "$APP/shared/pb_data.failed-$STAMP" || true; fi
127
+ # on_exit carries the original failure out; saying `die` here would only
128
+ # replace one non-zero code with another and re-enter this path.
129
+ mv -Tf "$PREVIOUS_DIR" "$PB_DATA" \
130
+ || log "could not put $PREVIOUS_DIR back at $PB_DATA - this instance needs a hand"
131
+ fi
132
+ rm -rf "$STAGE"
133
+ systemctl start "$PB_UNIT" >/dev/null 2>&1 || true
134
+ systemctl start "$WEB_UNIT" >/dev/null 2>&1 || true
135
+ }
136
+
137
+ on_exit() {
138
+ local code=$?
139
+ if [ "$RESTORED" = "0" ]; then revert; fi
140
+ exit "$code"
141
+ }
142
+ trap on_exit EXIT
143
+
144
+ log "stopping services"
145
+ systemctl stop "$WEB_UNIT" >/dev/null 2>&1 || true
146
+ systemctl stop "$PB_UNIT" >/dev/null 2>&1 || true
147
+
148
+ # Unpacked next to pb_data so that the swap below is a rename on one filesystem
149
+ # rather than a copy of everything the archive holds.
150
+ log "unpacking $(basename "$ARCHIVE")"
151
+ rm -rf "$STAGE"
152
+ mkdir -p "$STAGE"
153
+ unzip -q "$ARCHIVE" -d "$STAGE" || die "could not unpack $ARCHIVE"
154
+ [ -f "$STAGE/data.db" ] || die "$ARCHIVE unpacked without a data.db"
155
+
156
+ # The extraction ran as root. This -R is over a fresh tree of known size, which
157
+ # is the case the recursive form is actually for.
158
+ chown -R "$VELA_USER:$VELA_USER" "$STAGE"
159
+
160
+ # An archive taken while uploads were in S3 has no storage/ at all. Copied, not
161
+ # moved, so the directory set aside below stays a complete rollback.
162
+ if [ ! -d "$STAGE/storage" ] && [ -d "$PB_DATA/storage" ]; then
163
+ log "the archive has no uploads - keeping the ones already on disk"
164
+ cp -a "$PB_DATA/storage" "$STAGE/storage"
165
+ CARRIED_STORAGE=1
166
+ else
167
+ CARRIED_STORAGE=0
168
+ fi
169
+
170
+ # An archive taken from a later release than the one running carries a schema
171
+ # this code has not seen. `migrate up` cannot help - the archive is ahead, not
172
+ # behind - and `migrate down` is not an option either, because the migrations
173
+ # that would reverse it live in a release that may already have been pruned. Say
174
+ # so rather than restoring into a mismatch silently.
175
+ if [ -d "$APP/current/migrations" ]; then
176
+ # `|| true` because this is advisory: an archive old enough to predate the
177
+ # _migrations table would otherwise fail the pipeline and abort the restore.
178
+ AHEAD=$(sqlite3 "$STAGE/data.db" 'select file from _migrations' 2>/dev/null \
179
+ | while read -r file; do
180
+ [ -n "$file" ] || continue
181
+ [ -f "$APP/current/migrations/$file" ] || printf 'x'
182
+ done | wc -c | tr -d ' ' || true)
183
+ if [ "${AHEAD:-0}" -gt 0 ] 2>/dev/null; then
184
+ log "warning: this backup is $AHEAD migration(s) ahead of the running release"
185
+ log "warning: deploy the matching code, or expect the app to see columns it does not know"
186
+ fi
187
+ fi
188
+
189
+ log "replacing the database"
190
+ mv -Tf "$PB_DATA" "$PREVIOUS_DIR"
191
+ SWAPPED=1
192
+ mv -Tf "$STAGE" "$PB_DATA"
193
+
194
+ if [ "$MIGRATE" = "1" ] && [ -d "$APP/current/migrations" ]; then
195
+ log "running migrations"
196
+ runuser -u "$VELA_USER" -- "$APP/bin/pocketbase" \
197
+ --dir "$PB_DATA" \
198
+ --migrationsDir "$APP/current/migrations" \
199
+ migrate up >&2 \
200
+ || die "migrations failed against the restored database"
201
+ fi
202
+
203
+ # Unconditional, and after the migrations so that _superusers matches the schema
204
+ # the running release expects. --force because the env file did not change: the
205
+ # fingerprint cannot see that the database underneath it did.
206
+ reconcile_superuser "$APP" "$ETC" "$SU_EMAIL" "$SU_PASSWORD" --force
207
+
208
+ log "starting PocketBase on 127.0.0.1:$PB_PORT"
209
+ systemctl start "$PB_UNIT"
210
+ wait_for_http "http://127.0.0.1:$PB_PORT/api/health" 60 0.5 \
211
+ || die "PocketBase did not become healthy - journalctl -u $PB_UNIT"
212
+
213
+ # wait_for_http accepts 4xx, so the health check above passes even when the app
214
+ # would 401 on every render. Only an actual login proves the restore is usable.
215
+ assert_superuser_auth "$PB_PORT" "$SU_EMAIL" "$SU_PASSWORD" \
216
+ || die "the restored database does not accept this instance's superuser credentials"
217
+
218
+ log "starting app on 127.0.0.1:$WEB_PORT"
219
+ systemctl start "$WEB_UNIT"
220
+ wait_for_http "http://127.0.0.1:$WEB_PORT$HEALTH_PATH" 60 0.5 \
221
+ || die "app did not become healthy at $HEALTH_PATH - journalctl -u $WEB_UNIT"
222
+
223
+ RESTORED=1
224
+
225
+ # ------------------------------------------------------- the backup history
226
+ #
227
+ # PocketBase leaves backups/ out of the archives it writes, so the restored
228
+ # pb_data has none and every other snapshot on the box would otherwise be
229
+ # stranded in the directory set aside above. Moved only now that the restore has
230
+ # been proven: until this point any failure still has to be able to put the old
231
+ # directory back exactly as it was.
232
+
233
+ if [ -d "$PREVIOUS_DIR/backups" ]; then
234
+ log "carrying the backup history forward"
235
+ mkdir -p "$PB_DATA/backups"
236
+ find "$PREVIOUS_DIR/backups" -maxdepth 1 -type f -exec mv -t "$PB_DATA/backups" {} +
237
+ chown -R "$VELA_USER:$VELA_USER" "$PB_DATA/backups"
238
+ fi
239
+
240
+ # --------------------------------------------------------------------- state
241
+
242
+ state_merge "$INSTANCE" "$(jq -c -n \
243
+ --arg at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
244
+ --arg from "$(basename "$ARCHIVE")" \
245
+ --arg previous "$PREVIOUS_DIR" \
246
+ '{restoredAt: $at, restoredFrom: $from, previousDataDir: $previous}')"
247
+
248
+ # ------------------------------------------------------------------- cleanup
249
+
250
+ if [ "$CLEANUP_ARCHIVE" = "1" ]; then
251
+ rm -f "$ARCHIVE"
252
+ rmdir "$APP/shared/.restore" 2>/dev/null || true
253
+ fi
254
+
255
+ if [ "$KEEP_PREVIOUS" -gt 0 ] 2>/dev/null; then
256
+ mapfile -t old < <(ls -1d "$APP"/shared/pb_data.pre-restore-* 2>/dev/null | sort -r | tail -n +$((KEEP_PREVIOUS + 1)))
257
+ for dir in "${old[@]:-}"; do
258
+ [ -n "$dir" ] || continue
259
+ log "removing $(basename "$dir")"
260
+ rm -rf "$dir"
261
+ done
262
+ fi
263
+
264
+ emit_result \
265
+ --arg instance "$INSTANCE" --arg archive "$(basename "$ARCHIVE")" \
266
+ --arg previous "$PREVIOUS_DIR" --argjson migrated "$MIGRATE" \
267
+ --argjson storage "$CARRIED_STORAGE" \
268
+ '{instance: $instance, archive: $archive, previousDataDir: $previous,
269
+ migrated: ($migrated == 1), storageCarriedOver: ($storage == 1)}'
@@ -29,7 +29,7 @@
29
29
  "shadcn-svelte": "^1.2.7",
30
30
  "svelte": "^5.56.10",
31
31
  "svelte-check": "^4.6.0",
32
- "svelte-meta-tags": "^4.6.0",
32
+ "svelte-meta-tags": "^5.0.2",
33
33
  "svelte-sonner": "^1.1.0",
34
34
  "tailwind-merge": "^3.5.0",
35
35
  "tailwind-variants": "^3.2.2",
@@ -14,6 +14,11 @@ export default defineConfig({
14
14
  adapter: adapter({
15
15
  fallback: '200.html'
16
16
  }),
17
+ // Prerendering has no request to take an origin from, so without this
18
+ // every canonical link and `og:url` would be built from SvelteKit's
19
+ // placeholder host. `vela build` sets it from the domain the target is
20
+ // deployed on; unset, `$lib/site` still carries the site's own URL.
21
+ ...(process.env.VELA_ORIGIN ? { prerender: { origin: process.env.VELA_ORIGIN } } : {})
17
22
  })
18
23
  ]
19
24
  });