studio-console 1.3.4__tar.gz → 1.3.5__tar.gz
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.
- {studio_console-1.3.4 → studio_console-1.3.5}/PKG-INFO +64 -44
- {studio_console-1.3.4 → studio_console-1.3.5}/README.md +63 -43
- studio_console-1.3.5/VERSION +1 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/commands.py +61 -65
- studio_console-1.3.4/VERSION +0 -1
- {studio_console-1.3.4 → studio_console-1.3.5}/.gitignore +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/LEGAL.md +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/LICENSE +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/docker-compose.yml +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/install.sh +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/nginx/studio.conf.template +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/pyproject.toml +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/studio-console.py +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/__init__.py +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/__main__.py +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/cli.py +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/cloudflare/__init__.py +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/cloudflare/cf_api.py +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/cloudflare/cf_wizard.py +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/commands_container.py +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/commands_launch.py +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/constants.py +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/data/README.md +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/data/known_baselines.json +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/env.py +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/major_version.py +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/tui.py +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/wizard.py +0 -0
- {studio_console-1.3.4 → studio_console-1.3.5}/templates/.env.example +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: studio-console
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.5
|
|
4
4
|
Summary: Studio operator management console
|
|
5
5
|
Project-URL: Homepage, https://github.com/selfhosthub/studio-console
|
|
6
6
|
Project-URL: Issues, https://github.com/selfhosthub/studio-console/issues
|
|
@@ -24,9 +24,9 @@ Operator CLI for managing a self-hosted [Studio](https://github.com/selfhosthub/
|
|
|
24
24
|
|
|
25
25
|
Studio ships three images. Most operators want **Split**.
|
|
26
26
|
|
|
27
|
-
- **Split
|
|
28
|
-
- **Core** (`studio-core`)
|
|
29
|
-
- **Full** (`studio-full`
|
|
27
|
+
- **Split**: multi-container compose stack. Console runs on the host and manages `docker compose`. **This README documents Split.**
|
|
28
|
+
- **Core** (`studio-core`): single container bundling API + UI; you bring an external Postgres. Console runs inside the container.
|
|
29
|
+
- **Full** (`studio-full`): single container with bundled Postgres.
|
|
30
30
|
|
|
31
31
|
In Core and Full the container entrypoint provisions Studio on first boot; the console is a diagnostic + ops tool.
|
|
32
32
|
|
|
@@ -36,6 +36,7 @@ In Core and Full the container entrypoint provisions Studio on first boot; the c
|
|
|
36
36
|
|
|
37
37
|
- [Quick Start](#quick-start)
|
|
38
38
|
- [Run the Full image](#run-the-full-image)
|
|
39
|
+
- [Run the Core image](#run-the-core-image)
|
|
39
40
|
- [Daily Operations](#daily-operations)
|
|
40
41
|
- [Upgrading](#upgrading)
|
|
41
42
|
- [CLI Reference](#cli-reference)
|
|
@@ -52,15 +53,17 @@ In Core and Full the container entrypoint provisions Studio on first boot; the c
|
|
|
52
53
|
```sh
|
|
53
54
|
# 1. Install uv (one-time)
|
|
54
55
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
55
|
-
source ~/.bashrc # macOS default shell is zsh
|
|
56
|
+
source ~/.bashrc # macOS default shell is zsh, use ~/.zshrc instead
|
|
56
57
|
|
|
57
|
-
# 2. Install studio-console
|
|
58
|
-
uv tool install
|
|
58
|
+
# 2. Install studio-console (isolated CLI, recommended)
|
|
59
|
+
uv tool install studio-console
|
|
59
60
|
|
|
60
61
|
# 3. Run
|
|
61
62
|
studio-console
|
|
62
63
|
```
|
|
63
64
|
|
|
65
|
+
No uv and don't want it? `pip install studio-console` works anywhere Python 3.8+ is available. It installs into the active environment instead of an isolated one, so a virtualenv is a good idea.
|
|
66
|
+
|
|
64
67
|
On first run with no `~/.studio/.env`, the wizard launches. Walk through the sections, save, then go to **Services → Start all**.
|
|
65
68
|
|
|
66
69
|
The wizard creates `~/.studio/` containing `.env` (0600), `docker-compose.yml`, `nginx/studio.conf`, and four data subdirs:
|
|
@@ -69,15 +72,15 @@ The wizard creates `~/.studio/` containing `.env` (0600), `docker-compose.yml`,
|
|
|
69
72
|
~/.studio/
|
|
70
73
|
├── .env, .bootstrapped, docker-compose.yml, nginx/
|
|
71
74
|
├── db/ postgres data (SHS_DB_DATA)
|
|
72
|
-
├── storage/ orgs, uploads, outputs (SHS_STORAGE_ROOT
|
|
75
|
+
├── storage/ orgs, uploads, outputs (SHS_STORAGE_ROOT, mounted at /workspace in containers)
|
|
73
76
|
├── models/ model files (SHS_MODELS_ROOT)
|
|
74
77
|
└── backups/ local DB dumps (SHS_BACKUP_ROOT)
|
|
75
78
|
```
|
|
76
79
|
|
|
77
|
-
Each data subdir is its own env var, so a cloud deploy can repoint individual roots at CloudSQL, GCS, or a network volume without changing code. **Protect this directory and never delete `.env
|
|
80
|
+
Each data subdir is its own env var, so a cloud deploy can repoint individual roots at CloudSQL, GCS, or a network volume without changing code. **Protect this directory and never delete `.env`.** Two of its values cannot be regenerated:
|
|
78
81
|
|
|
79
|
-
- `SHS_CREDENTIAL_ENCRYPTION_KEY
|
|
80
|
-
- `POSTGRES_PASSWORD
|
|
82
|
+
- `SHS_CREDENTIAL_ENCRYPTION_KEY`: losing it makes all stored provider API keys unrecoverable.
|
|
83
|
+
- `POSTGRES_PASSWORD`: set on first DB init; the live database keeps using this value, so a regenerated one will lock the API out.
|
|
81
84
|
|
|
82
85
|
Back both up separately (password manager, secrets vault) so you can recover if the host or `.env` is ever lost.
|
|
83
86
|
|
|
@@ -96,23 +99,23 @@ Full list of supported environment variables in [docs/env-vars.md](docs/env-vars
|
|
|
96
99
|
|
|
97
100
|
## Run the Full image
|
|
98
101
|
|
|
99
|
-
The **Full** image (`studio-full`) is a single self-contained container
|
|
102
|
+
The **Full** image (`studio-full`) is a single self-contained container with bundled Postgres, API, UI, and workers under supervisord. No Compose stack, no external database. `launch-full` runs it on your machine and drops you into its in-container console.
|
|
100
103
|
|
|
101
104
|
```sh
|
|
102
|
-
# Launch (workspace defaults to ~/.studio, shared with Split)
|
|
103
|
-
studio-console launch-full
|
|
105
|
+
# Launch (defaults to the latest tag; workspace defaults to ~/.studio, shared with Split)
|
|
106
|
+
studio-console launch-full
|
|
104
107
|
|
|
105
|
-
#
|
|
106
|
-
studio-console launch-full --tag 1.
|
|
108
|
+
# Pin a tag and/or use a separate workspace to run Full alongside a Split install
|
|
109
|
+
studio-console launch-full --tag 1.2.4 --workspace ~/.studio-full
|
|
107
110
|
```
|
|
108
111
|
|
|
109
|
-
The workspace (mounted at `/workspace`) holds the generated `.env`, the Postgres data dir, and org files
|
|
112
|
+
The workspace (mounted at `/workspace`) holds the generated `.env`, the Postgres data dir, and org files; it persists across restarts. You're prompted once for a supervisor username/password; the console remembers them and re-injects on every launch.
|
|
110
113
|
|
|
111
114
|
> **Do not point Full at an existing Split `~/.studio` workspace** unless you mean to share it. Split and Full share the same `.env`/encryption key but **must never run concurrently** against the same data. Use `--workspace` to keep them separate.
|
|
112
115
|
|
|
113
116
|
### Re-entering the console after you exit
|
|
114
117
|
|
|
115
|
-
Exiting the console **does not stop the container
|
|
118
|
+
Exiting the console **does not stop the container**; it keeps running. `launch-full` won't re-attach to an already-running container; instead, exec back in from the host:
|
|
116
119
|
|
|
117
120
|
```sh
|
|
118
121
|
docker exec -it studio-full studio-console # re-open the in-container menu
|
|
@@ -127,6 +130,38 @@ Inside the container the console manages services via `supervisorctl` (health, r
|
|
|
127
130
|
|
|
128
131
|
---
|
|
129
132
|
|
|
133
|
+
## Run the Core image
|
|
134
|
+
|
|
135
|
+
The **Core** image (`studio-core`) is the same single-container API + UI as Full, but without a bundled database. You bring your own Postgres (a managed cloud instance, a RunPod sidecar, any reachable server). `launch-core` runs it on your machine and drops you into its in-container console.
|
|
136
|
+
|
|
137
|
+
Point Core at your database first, then launch:
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
# Set the external database URL (prompts if you omit it)
|
|
141
|
+
studio-console core-db-url postgresql+asyncpg://user:pass@host:5432/studio
|
|
142
|
+
|
|
143
|
+
# Launch (defaults to the latest tag; workspace defaults to ~/.studio-core)
|
|
144
|
+
studio-console launch-core
|
|
145
|
+
|
|
146
|
+
# Pin a tag and/or use a separate workspace
|
|
147
|
+
studio-console launch-core --tag 1.2.4 --workspace ~/.studio-core
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Precedence for the database URL: a saved value from `core-db-url`, then the `SHS_DATABASE_URL` environment variable, then an interactive prompt. Export the env var to boot Core unattended against a remote Postgres with no prompt:
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
SHS_DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/studio studio-console launch-core
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Re-enter and run one-off commands the same way as Full, swapping the container name:
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
docker exec -it studio-core studio-console # re-open the in-container menu
|
|
160
|
+
docker exec studio-core studio-console health
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
130
165
|
## Daily Operations
|
|
131
166
|
|
|
132
167
|
```
|
|
@@ -153,33 +188,15 @@ Backups land in `~/.studio/backups/studio-YYYYMMDD_HHMMSS/`.
|
|
|
153
188
|
Images → Upgrade
|
|
154
189
|
```
|
|
155
190
|
|
|
156
|
-
Pulls the latest tag from GHCR, updates `SHS_STUDIO_VERSION` in `.env`, and restarts all services. If the new version's major doesn't match your database's major, the upgrade is blocked with a clear message
|
|
191
|
+
Pulls the latest tag from GHCR, updates `SHS_STUDIO_VERSION` in `.env`, and restarts all services. If the new version's major doesn't match your database's major, the upgrade is blocked with a clear message. See [docs/architecture.md](docs/architecture.md#major-version-boundary-detection).
|
|
157
192
|
|
|
158
193
|
**studio-console itself:**
|
|
159
194
|
|
|
160
195
|
```bash
|
|
161
|
-
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
Latest release is always at [github.com/selfhosthub/studio-console/releases/latest](https://github.com/selfhosthub/studio-console/releases/latest).
|
|
165
|
-
|
|
166
|
-
---
|
|
167
|
-
|
|
168
|
-
## Cutting a release
|
|
169
|
-
|
|
170
|
-
**One command. Never tag, bump `VERSION`, or run `gh release` by hand** — the script does all of it (bumps `VERSION`, updates the README install URL, commits, pushes, tags, builds the wheel, publishes the release with the correct title).
|
|
171
|
-
|
|
172
|
-
```bash
|
|
173
|
-
# 1. Land your fix on main first (commit + push as normal).
|
|
174
|
-
# 2. Then cut the release — pick the bump size:
|
|
175
|
-
scripts/release-console.sh patch # bug fix 1.0.0 → 1.0.1
|
|
176
|
-
scripts/release-console.sh minor # new feature 1.0.0 → 1.1.0
|
|
177
|
-
scripts/release-console.sh major # breaking 1.0.0 → 2.0.0
|
|
196
|
+
studio-console self-update
|
|
178
197
|
```
|
|
179
198
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
After release, bump the console pin in the Studio versions file to the new version so the next core/full image build bakes it in.
|
|
199
|
+
`self-update` detects how Console was installed (uv, pip, or Homebrew) and upgrades in place.
|
|
183
200
|
|
|
184
201
|
---
|
|
185
202
|
|
|
@@ -200,6 +217,7 @@ studio-console restore [path] # restore from backup directory
|
|
|
200
217
|
studio-console links # print service URLs
|
|
201
218
|
studio-console config # show current .env values
|
|
202
219
|
studio-console config set KEY VALUE # set a single .env value
|
|
220
|
+
studio-console config unset KEY # remove a single .env value
|
|
203
221
|
studio-console workers # list/scale workers
|
|
204
222
|
studio-console reset-password # reset super admin password
|
|
205
223
|
studio-console wizard # re-run setup wizard
|
|
@@ -208,18 +226,20 @@ studio-console self-update # upgrade studio-console itself
|
|
|
208
226
|
studio-console version # print version
|
|
209
227
|
|
|
210
228
|
studio-console launch-full [--tag T] [--workspace DIR] # run the Full single-container image on the host
|
|
229
|
+
studio-console launch-core [--tag T] [--workspace DIR] # run the Core single-container image (external Postgres)
|
|
230
|
+
studio-console core-db-url [URL] # set Core's external database URL for the next launch-core
|
|
211
231
|
```
|
|
212
232
|
|
|
213
|
-
For the Full image, run operational subcommands through `docker exec studio-full studio-console <cmd>`
|
|
233
|
+
For the Full or Core image, run operational subcommands through `docker exec studio-full studio-console <cmd>` (or `studio-core`). See [Run the Full image](#run-the-full-image) and [Run the Core image](#run-the-core-image).
|
|
214
234
|
|
|
215
235
|
---
|
|
216
236
|
|
|
217
237
|
## Further reading
|
|
218
238
|
|
|
219
|
-
- **[Architecture](docs/architecture.md)
|
|
220
|
-
- **[Environment variables](docs/env-vars.md)
|
|
221
|
-
- **[Public hostname topology](docs/topology.md)
|
|
222
|
-
- **[VPS + RunPod deployments](docs/vps-runpod.md)
|
|
239
|
+
- **[Architecture](docs/architecture.md)**: file layout, state flow, Compose wiring, first-boot, internals (wizard vs init, orphan workers, backup format, major-version detection, deployment contexts).
|
|
240
|
+
- **[Environment variables](docs/env-vars.md)**: every `SHS_*` and supporting var.
|
|
241
|
+
- **[Public hostname topology](docs/topology.md)**: single vs split hostnames, Cloudflare tunnel + Access, IP restrictions.
|
|
242
|
+
- **[VPS + RunPod deployments](docs/vps-runpod.md)**: hybrid setups with GPU worker pods on RunPod.
|
|
223
243
|
|
|
224
244
|
---
|
|
225
245
|
|
|
@@ -6,9 +6,9 @@ Operator CLI for managing a self-hosted [Studio](https://github.com/selfhosthub/
|
|
|
6
6
|
|
|
7
7
|
Studio ships three images. Most operators want **Split**.
|
|
8
8
|
|
|
9
|
-
- **Split
|
|
10
|
-
- **Core** (`studio-core`)
|
|
11
|
-
- **Full** (`studio-full`
|
|
9
|
+
- **Split**: multi-container compose stack. Console runs on the host and manages `docker compose`. **This README documents Split.**
|
|
10
|
+
- **Core** (`studio-core`): single container bundling API + UI; you bring an external Postgres. Console runs inside the container.
|
|
11
|
+
- **Full** (`studio-full`): single container with bundled Postgres.
|
|
12
12
|
|
|
13
13
|
In Core and Full the container entrypoint provisions Studio on first boot; the console is a diagnostic + ops tool.
|
|
14
14
|
|
|
@@ -18,6 +18,7 @@ In Core and Full the container entrypoint provisions Studio on first boot; the c
|
|
|
18
18
|
|
|
19
19
|
- [Quick Start](#quick-start)
|
|
20
20
|
- [Run the Full image](#run-the-full-image)
|
|
21
|
+
- [Run the Core image](#run-the-core-image)
|
|
21
22
|
- [Daily Operations](#daily-operations)
|
|
22
23
|
- [Upgrading](#upgrading)
|
|
23
24
|
- [CLI Reference](#cli-reference)
|
|
@@ -34,15 +35,17 @@ In Core and Full the container entrypoint provisions Studio on first boot; the c
|
|
|
34
35
|
```sh
|
|
35
36
|
# 1. Install uv (one-time)
|
|
36
37
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
37
|
-
source ~/.bashrc # macOS default shell is zsh
|
|
38
|
+
source ~/.bashrc # macOS default shell is zsh, use ~/.zshrc instead
|
|
38
39
|
|
|
39
|
-
# 2. Install studio-console
|
|
40
|
-
uv tool install
|
|
40
|
+
# 2. Install studio-console (isolated CLI, recommended)
|
|
41
|
+
uv tool install studio-console
|
|
41
42
|
|
|
42
43
|
# 3. Run
|
|
43
44
|
studio-console
|
|
44
45
|
```
|
|
45
46
|
|
|
47
|
+
No uv and don't want it? `pip install studio-console` works anywhere Python 3.8+ is available. It installs into the active environment instead of an isolated one, so a virtualenv is a good idea.
|
|
48
|
+
|
|
46
49
|
On first run with no `~/.studio/.env`, the wizard launches. Walk through the sections, save, then go to **Services → Start all**.
|
|
47
50
|
|
|
48
51
|
The wizard creates `~/.studio/` containing `.env` (0600), `docker-compose.yml`, `nginx/studio.conf`, and four data subdirs:
|
|
@@ -51,15 +54,15 @@ The wizard creates `~/.studio/` containing `.env` (0600), `docker-compose.yml`,
|
|
|
51
54
|
~/.studio/
|
|
52
55
|
├── .env, .bootstrapped, docker-compose.yml, nginx/
|
|
53
56
|
├── db/ postgres data (SHS_DB_DATA)
|
|
54
|
-
├── storage/ orgs, uploads, outputs (SHS_STORAGE_ROOT
|
|
57
|
+
├── storage/ orgs, uploads, outputs (SHS_STORAGE_ROOT, mounted at /workspace in containers)
|
|
55
58
|
├── models/ model files (SHS_MODELS_ROOT)
|
|
56
59
|
└── backups/ local DB dumps (SHS_BACKUP_ROOT)
|
|
57
60
|
```
|
|
58
61
|
|
|
59
|
-
Each data subdir is its own env var, so a cloud deploy can repoint individual roots at CloudSQL, GCS, or a network volume without changing code. **Protect this directory and never delete `.env
|
|
62
|
+
Each data subdir is its own env var, so a cloud deploy can repoint individual roots at CloudSQL, GCS, or a network volume without changing code. **Protect this directory and never delete `.env`.** Two of its values cannot be regenerated:
|
|
60
63
|
|
|
61
|
-
- `SHS_CREDENTIAL_ENCRYPTION_KEY
|
|
62
|
-
- `POSTGRES_PASSWORD
|
|
64
|
+
- `SHS_CREDENTIAL_ENCRYPTION_KEY`: losing it makes all stored provider API keys unrecoverable.
|
|
65
|
+
- `POSTGRES_PASSWORD`: set on first DB init; the live database keeps using this value, so a regenerated one will lock the API out.
|
|
63
66
|
|
|
64
67
|
Back both up separately (password manager, secrets vault) so you can recover if the host or `.env` is ever lost.
|
|
65
68
|
|
|
@@ -78,23 +81,23 @@ Full list of supported environment variables in [docs/env-vars.md](docs/env-vars
|
|
|
78
81
|
|
|
79
82
|
## Run the Full image
|
|
80
83
|
|
|
81
|
-
The **Full** image (`studio-full`) is a single self-contained container
|
|
84
|
+
The **Full** image (`studio-full`) is a single self-contained container with bundled Postgres, API, UI, and workers under supervisord. No Compose stack, no external database. `launch-full` runs it on your machine and drops you into its in-container console.
|
|
82
85
|
|
|
83
86
|
```sh
|
|
84
|
-
# Launch (workspace defaults to ~/.studio, shared with Split)
|
|
85
|
-
studio-console launch-full
|
|
87
|
+
# Launch (defaults to the latest tag; workspace defaults to ~/.studio, shared with Split)
|
|
88
|
+
studio-console launch-full
|
|
86
89
|
|
|
87
|
-
#
|
|
88
|
-
studio-console launch-full --tag 1.
|
|
90
|
+
# Pin a tag and/or use a separate workspace to run Full alongside a Split install
|
|
91
|
+
studio-console launch-full --tag 1.2.4 --workspace ~/.studio-full
|
|
89
92
|
```
|
|
90
93
|
|
|
91
|
-
The workspace (mounted at `/workspace`) holds the generated `.env`, the Postgres data dir, and org files
|
|
94
|
+
The workspace (mounted at `/workspace`) holds the generated `.env`, the Postgres data dir, and org files; it persists across restarts. You're prompted once for a supervisor username/password; the console remembers them and re-injects on every launch.
|
|
92
95
|
|
|
93
96
|
> **Do not point Full at an existing Split `~/.studio` workspace** unless you mean to share it. Split and Full share the same `.env`/encryption key but **must never run concurrently** against the same data. Use `--workspace` to keep them separate.
|
|
94
97
|
|
|
95
98
|
### Re-entering the console after you exit
|
|
96
99
|
|
|
97
|
-
Exiting the console **does not stop the container
|
|
100
|
+
Exiting the console **does not stop the container**; it keeps running. `launch-full` won't re-attach to an already-running container; instead, exec back in from the host:
|
|
98
101
|
|
|
99
102
|
```sh
|
|
100
103
|
docker exec -it studio-full studio-console # re-open the in-container menu
|
|
@@ -109,6 +112,38 @@ Inside the container the console manages services via `supervisorctl` (health, r
|
|
|
109
112
|
|
|
110
113
|
---
|
|
111
114
|
|
|
115
|
+
## Run the Core image
|
|
116
|
+
|
|
117
|
+
The **Core** image (`studio-core`) is the same single-container API + UI as Full, but without a bundled database. You bring your own Postgres (a managed cloud instance, a RunPod sidecar, any reachable server). `launch-core` runs it on your machine and drops you into its in-container console.
|
|
118
|
+
|
|
119
|
+
Point Core at your database first, then launch:
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
# Set the external database URL (prompts if you omit it)
|
|
123
|
+
studio-console core-db-url postgresql+asyncpg://user:pass@host:5432/studio
|
|
124
|
+
|
|
125
|
+
# Launch (defaults to the latest tag; workspace defaults to ~/.studio-core)
|
|
126
|
+
studio-console launch-core
|
|
127
|
+
|
|
128
|
+
# Pin a tag and/or use a separate workspace
|
|
129
|
+
studio-console launch-core --tag 1.2.4 --workspace ~/.studio-core
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Precedence for the database URL: a saved value from `core-db-url`, then the `SHS_DATABASE_URL` environment variable, then an interactive prompt. Export the env var to boot Core unattended against a remote Postgres with no prompt:
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
SHS_DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/studio studio-console launch-core
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Re-enter and run one-off commands the same way as Full, swapping the container name:
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
docker exec -it studio-core studio-console # re-open the in-container menu
|
|
142
|
+
docker exec studio-core studio-console health
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
112
147
|
## Daily Operations
|
|
113
148
|
|
|
114
149
|
```
|
|
@@ -135,33 +170,15 @@ Backups land in `~/.studio/backups/studio-YYYYMMDD_HHMMSS/`.
|
|
|
135
170
|
Images → Upgrade
|
|
136
171
|
```
|
|
137
172
|
|
|
138
|
-
Pulls the latest tag from GHCR, updates `SHS_STUDIO_VERSION` in `.env`, and restarts all services. If the new version's major doesn't match your database's major, the upgrade is blocked with a clear message
|
|
173
|
+
Pulls the latest tag from GHCR, updates `SHS_STUDIO_VERSION` in `.env`, and restarts all services. If the new version's major doesn't match your database's major, the upgrade is blocked with a clear message. See [docs/architecture.md](docs/architecture.md#major-version-boundary-detection).
|
|
139
174
|
|
|
140
175
|
**studio-console itself:**
|
|
141
176
|
|
|
142
177
|
```bash
|
|
143
|
-
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
Latest release is always at [github.com/selfhosthub/studio-console/releases/latest](https://github.com/selfhosthub/studio-console/releases/latest).
|
|
147
|
-
|
|
148
|
-
---
|
|
149
|
-
|
|
150
|
-
## Cutting a release
|
|
151
|
-
|
|
152
|
-
**One command. Never tag, bump `VERSION`, or run `gh release` by hand** — the script does all of it (bumps `VERSION`, updates the README install URL, commits, pushes, tags, builds the wheel, publishes the release with the correct title).
|
|
153
|
-
|
|
154
|
-
```bash
|
|
155
|
-
# 1. Land your fix on main first (commit + push as normal).
|
|
156
|
-
# 2. Then cut the release — pick the bump size:
|
|
157
|
-
scripts/release-console.sh patch # bug fix 1.0.0 → 1.0.1
|
|
158
|
-
scripts/release-console.sh minor # new feature 1.0.0 → 1.1.0
|
|
159
|
-
scripts/release-console.sh major # breaking 1.0.0 → 2.0.0
|
|
178
|
+
studio-console self-update
|
|
160
179
|
```
|
|
161
180
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
After release, bump the console pin in the Studio versions file to the new version so the next core/full image build bakes it in.
|
|
181
|
+
`self-update` detects how Console was installed (uv, pip, or Homebrew) and upgrades in place.
|
|
165
182
|
|
|
166
183
|
---
|
|
167
184
|
|
|
@@ -182,6 +199,7 @@ studio-console restore [path] # restore from backup directory
|
|
|
182
199
|
studio-console links # print service URLs
|
|
183
200
|
studio-console config # show current .env values
|
|
184
201
|
studio-console config set KEY VALUE # set a single .env value
|
|
202
|
+
studio-console config unset KEY # remove a single .env value
|
|
185
203
|
studio-console workers # list/scale workers
|
|
186
204
|
studio-console reset-password # reset super admin password
|
|
187
205
|
studio-console wizard # re-run setup wizard
|
|
@@ -190,18 +208,20 @@ studio-console self-update # upgrade studio-console itself
|
|
|
190
208
|
studio-console version # print version
|
|
191
209
|
|
|
192
210
|
studio-console launch-full [--tag T] [--workspace DIR] # run the Full single-container image on the host
|
|
211
|
+
studio-console launch-core [--tag T] [--workspace DIR] # run the Core single-container image (external Postgres)
|
|
212
|
+
studio-console core-db-url [URL] # set Core's external database URL for the next launch-core
|
|
193
213
|
```
|
|
194
214
|
|
|
195
|
-
For the Full image, run operational subcommands through `docker exec studio-full studio-console <cmd>`
|
|
215
|
+
For the Full or Core image, run operational subcommands through `docker exec studio-full studio-console <cmd>` (or `studio-core`). See [Run the Full image](#run-the-full-image) and [Run the Core image](#run-the-core-image).
|
|
196
216
|
|
|
197
217
|
---
|
|
198
218
|
|
|
199
219
|
## Further reading
|
|
200
220
|
|
|
201
|
-
- **[Architecture](docs/architecture.md)
|
|
202
|
-
- **[Environment variables](docs/env-vars.md)
|
|
203
|
-
- **[Public hostname topology](docs/topology.md)
|
|
204
|
-
- **[VPS + RunPod deployments](docs/vps-runpod.md)
|
|
221
|
+
- **[Architecture](docs/architecture.md)**: file layout, state flow, Compose wiring, first-boot, internals (wizard vs init, orphan workers, backup format, major-version detection, deployment contexts).
|
|
222
|
+
- **[Environment variables](docs/env-vars.md)**: every `SHS_*` and supporting var.
|
|
223
|
+
- **[Public hostname topology](docs/topology.md)**: single vs split hostnames, Cloudflare tunnel + Access, IP restrictions.
|
|
224
|
+
- **[VPS + RunPod deployments](docs/vps-runpod.md)**: hybrid setups with GPU worker pods on RunPod.
|
|
205
225
|
|
|
206
226
|
---
|
|
207
227
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.3.5
|
|
@@ -76,7 +76,7 @@ from .wizard import (
|
|
|
76
76
|
def _apply_scale_flags(up_cmd: list[str], env_data: dict) -> list[str]:
|
|
77
77
|
"""Append --scale flags for any *active* worker with a count != 1.
|
|
78
78
|
|
|
79
|
-
Only scales services whose profile is in COMPOSE_PROFILES
|
|
79
|
+
Only scales services whose profile is in COMPOSE_PROFILES, compose errors
|
|
80
80
|
'no such service: X: disabled' if you --scale a profile that isn't active.
|
|
81
81
|
"""
|
|
82
82
|
active = {p for p in env_data.get("COMPOSE_PROFILES", "").split(",") if p}
|
|
@@ -92,7 +92,7 @@ def _apply_scale_flags(up_cmd: list[str], env_data: dict) -> list[str]:
|
|
|
92
92
|
def _stop_api_ui_containers() -> None:
|
|
93
93
|
"""Remove all studio-api-*, studio-ui-*, studio-nginx-* containers directly.
|
|
94
94
|
|
|
95
|
-
Uses 'ps -a' so Created/Exited containers (not just running ones) are caught
|
|
95
|
+
Uses 'ps -a' so Created/Exited containers (not just running ones) are caught
|
|
96
96
|
otherwise a leftover 'studio-api-2' in Created state collides with compose on
|
|
97
97
|
recreate ('container name already in use'). 'docker rm -f' force-removes
|
|
98
98
|
running ones too; '-s' is NOT a valid rm flag.
|
|
@@ -167,7 +167,7 @@ def config_menu(context: str, env_file: Path) -> None:
|
|
|
167
167
|
|
|
168
168
|
menu_options = [
|
|
169
169
|
f"Services {_dim('start · stop · restart · health · logs · links')}",
|
|
170
|
-
f"Setup {_dim('wizard
|
|
170
|
+
f"Setup {_dim('wizard: components · secrets · domain · cloudflare')}",
|
|
171
171
|
f"Images {_dim('build · upgrade · rollback')}",
|
|
172
172
|
f"Advanced {_dim('scale API/UI · per-service ops · cloudflare')}",
|
|
173
173
|
f"Backup {_dim('backup · restore')}",
|
|
@@ -212,7 +212,7 @@ def config_menu(context: str, env_file: Path) -> None:
|
|
|
212
212
|
print(f" Status: {_cyan('configured')} | Services → Start all")
|
|
213
213
|
elif mv.scrape_guardrail_failure(env_file, context):
|
|
214
214
|
print(
|
|
215
|
-
f" Status: {_red('blocked')} | major-version boundary
|
|
215
|
+
f" Status: {_red('blocked')} | major-version boundary, see Services → Health"
|
|
216
216
|
)
|
|
217
217
|
else:
|
|
218
218
|
print(f" Status: {_yellow('starting...')}")
|
|
@@ -265,7 +265,7 @@ def _restart_for_setup(
|
|
|
265
265
|
base = compose_cmd(env_file)
|
|
266
266
|
new_profiles = {p for p in env_data.get("COMPOSE_PROFILES", "").split(",") if p}
|
|
267
267
|
|
|
268
|
-
# Resolve missing images BEFORE stopping anything
|
|
268
|
+
# Resolve missing images BEFORE stopping anything, a missing image must not
|
|
269
269
|
# leave a running stack torn down. In source mode we build; in registry mode
|
|
270
270
|
# we pull. Only if a pull fails do we bail, with everything still up.
|
|
271
271
|
missing = _missing_images(env_file)
|
|
@@ -279,10 +279,10 @@ def _restart_for_setup(
|
|
|
279
279
|
if not _pull_images(missing, tag):
|
|
280
280
|
error(f"Failed to pull images: {', '.join(missing)}")
|
|
281
281
|
warn("Check the tag (SHS_STUDIO_VERSION) and registry access.")
|
|
282
|
-
warn("No changes applied
|
|
282
|
+
warn("No changes applied, services left as they were.")
|
|
283
283
|
return
|
|
284
284
|
|
|
285
|
-
# Stop workers directly
|
|
285
|
+
# Stop workers directly, catches orphans whose profiles were removed from .env.
|
|
286
286
|
# 'ps -a' so Created/Exited workers are removed too (avoids name collisions on
|
|
287
287
|
# recreate); 'rm -f' force-removes running ones ('-s' is not a valid rm flag).
|
|
288
288
|
_, ps_out = run_quiet(["docker", "ps", "-aq", "--filter", "name=studio-worker-"])
|
|
@@ -302,7 +302,7 @@ def _restart_for_setup(
|
|
|
302
302
|
timeout=120,
|
|
303
303
|
)
|
|
304
304
|
else:
|
|
305
|
-
# Workers only
|
|
305
|
+
# Workers only, explicit service names so API/UI/nginx are never touched
|
|
306
306
|
worker_services = [
|
|
307
307
|
svc for svc in SCALE_PROFILES.values() if svc in new_profiles
|
|
308
308
|
]
|
|
@@ -315,7 +315,7 @@ def _restart_for_setup(
|
|
|
315
315
|
timeout=120,
|
|
316
316
|
)
|
|
317
317
|
else:
|
|
318
|
-
info("No active worker profiles
|
|
318
|
+
info("No active worker profiles, nothing to start.")
|
|
319
319
|
ok("Done")
|
|
320
320
|
except Exception as e:
|
|
321
321
|
error(f"Docker Compose failed: {e}")
|
|
@@ -327,13 +327,13 @@ def _restart_for_setup(
|
|
|
327
327
|
|
|
328
328
|
|
|
329
329
|
def _submenu_setup(context: str, env_file: Path) -> None:
|
|
330
|
-
"""Setup submenu
|
|
330
|
+
"""Setup submenu, runs the wizard then offers Apply now / Skip."""
|
|
331
331
|
old_profiles = (
|
|
332
332
|
read_env(env_file).get("COMPOSE_PROFILES", "") if env_file.exists() else ""
|
|
333
333
|
)
|
|
334
334
|
if wizard(context, env_file):
|
|
335
335
|
apply_options = [
|
|
336
|
-
f"Apply now {_dim('restarts changed services
|
|
336
|
+
f"Apply now {_dim('restarts changed services, brief downtime possible')}",
|
|
337
337
|
f"Skip {_dim('apply on next manual restart')}",
|
|
338
338
|
]
|
|
339
339
|
pick = _interactive_single(
|
|
@@ -364,7 +364,7 @@ def _cmd_scale_api_ui(env_file: Path) -> None:
|
|
|
364
364
|
ok("Scaling updated")
|
|
365
365
|
if not _interactive_yn("Restart API + UI to apply?", default=True):
|
|
366
366
|
return
|
|
367
|
-
warn_header("This will restart API, UI, and nginx
|
|
367
|
+
warn_header("This will restart API, UI, and nginx. Workers will keep running")
|
|
368
368
|
_stop_api_ui_containers()
|
|
369
369
|
env_data = read_env(env_file)
|
|
370
370
|
run(
|
|
@@ -377,7 +377,7 @@ def _cmd_scale_api_ui(env_file: Path) -> None:
|
|
|
377
377
|
|
|
378
378
|
|
|
379
379
|
def _submenu_services(context: str, env_file: Path) -> None:
|
|
380
|
-
"""Services submenu
|
|
380
|
+
"""Services submenu, daily ops: start/stop/restart all, health, logs, links."""
|
|
381
381
|
options = [
|
|
382
382
|
f"Start all {_dim('pull/build if needed, then start')}",
|
|
383
383
|
f"Stop all {_dim('stop all containers')}",
|
|
@@ -459,7 +459,7 @@ def _owns_postgres(env_data: dict) -> bool:
|
|
|
459
459
|
"""True if SHS_DATABASE_URL points at a postgres instance we manage.
|
|
460
460
|
|
|
461
461
|
External DBs (CloudSQL, RDS, etc., or a CloudSQL Auth Proxy sidecar) own
|
|
462
|
-
their own backup tooling
|
|
462
|
+
their own backup tooling, console refuses backup/restore in those cases
|
|
463
463
|
rather than producing dumps that can't be restored back to the source.
|
|
464
464
|
"""
|
|
465
465
|
url = env_data.get("SHS_DATABASE_URL", "")
|
|
@@ -511,7 +511,7 @@ def _submenu_backup(context: str, env_file: Path) -> None:
|
|
|
511
511
|
def cmd_db_role(context: str, env_file: Path) -> None:
|
|
512
512
|
"""Show/enable the restricted runtime DB role (shs_app cutover).
|
|
513
513
|
|
|
514
|
-
Console only writes SHS_DATABASE_APP_URL
|
|
514
|
+
Console only writes SHS_DATABASE_APP_URL, the API's bootstrap provisions
|
|
515
515
|
the role, grants, and RLS posture from it on every boot. SHS_DATABASE_URL
|
|
516
516
|
stays privileged; console's own psql/dump/restore tooling keeps using it.
|
|
517
517
|
"""
|
|
@@ -527,10 +527,10 @@ def cmd_db_role(context: str, env_file: Path) -> None:
|
|
|
527
527
|
"SHS_DATABASE_URL", ""
|
|
528
528
|
)
|
|
529
529
|
if not db_url:
|
|
530
|
-
error("No SHS_DATABASE_URL found
|
|
530
|
+
error("No SHS_DATABASE_URL found. Configure the database first.")
|
|
531
531
|
return
|
|
532
532
|
|
|
533
|
-
warn_header("The API currently connects as the privileged DB role
|
|
533
|
+
warn_header("The API currently connects as the privileged DB role; RLS is inert")
|
|
534
534
|
info("Enabling writes SHS_DATABASE_APP_URL (role shs_app) next to SHS_DATABASE_URL.")
|
|
535
535
|
info("The API provisions the role itself on next boot; console runs no SQL.")
|
|
536
536
|
info(_dim("Requires a studio image with restricted-role support; older images ignore it."))
|
|
@@ -542,7 +542,7 @@ def cmd_db_role(context: str, env_file: Path) -> None:
|
|
|
542
542
|
print()
|
|
543
543
|
|
|
544
544
|
if not _interactive_yn("Enable the restricted DB role?", default=False):
|
|
545
|
-
info("Skipped
|
|
545
|
+
info("Skipped, no changes made.")
|
|
546
546
|
return
|
|
547
547
|
|
|
548
548
|
set_env_value(env_file, "SHS_DATABASE_APP_URL", derive_app_db_url(db_url))
|
|
@@ -560,26 +560,26 @@ def cmd_db_role(context: str, env_file: Path) -> None:
|
|
|
560
560
|
),
|
|
561
561
|
timeout=120,
|
|
562
562
|
)
|
|
563
|
-
ok("Services restarted
|
|
563
|
+
ok("Services restarted. Check Services → Health for the RLS posture.")
|
|
564
564
|
else:
|
|
565
565
|
info("Takes effect on the next restart of the API.")
|
|
566
566
|
else:
|
|
567
567
|
warn(
|
|
568
|
-
"Restart this container from the host to apply
|
|
568
|
+
"Restart this container from the host to apply, provisioning runs in "
|
|
569
569
|
"the container entrypoint, not under supervisord."
|
|
570
570
|
)
|
|
571
571
|
print(f" {_bold('docker restart <container>')} {_dim('(or stop/start the pod on RunPod)')}")
|
|
572
572
|
|
|
573
573
|
|
|
574
574
|
def _submenu_advanced(context: str, env_file: Path) -> None:
|
|
575
|
-
"""Advanced submenu
|
|
575
|
+
"""Advanced submenu, scale API/UI, per-service ops, Cloudflare ops."""
|
|
576
576
|
options = [
|
|
577
577
|
f"Scale API/UI {_dim('set replica count, enable/disable nginx LB')}",
|
|
578
578
|
f"Start one {_dim('start a stopped service')}",
|
|
579
579
|
f"Stop one {_dim('stop a running service')}",
|
|
580
580
|
f"Restart one {_dim('restart a single service')}",
|
|
581
581
|
f"Show .env {_dim('current configuration values')}",
|
|
582
|
-
f"DB role {_dim('restricted runtime role (RLS)
|
|
582
|
+
f"DB role {_dim('restricted runtime role (RLS): status · enable')}",
|
|
583
583
|
f"Cloudflare {_dim('tunnel · routes · IP rules · Access')}",
|
|
584
584
|
]
|
|
585
585
|
idx = _interactive_single("Advanced", options, default=0)
|
|
@@ -759,7 +759,7 @@ def _submenu_cloudflare(context: str, env_file: Path) -> None:
|
|
|
759
759
|
|
|
760
760
|
_sync_derived_urls(env_file)
|
|
761
761
|
ok(f"Domain updated to {new_domain.rstrip('/')}")
|
|
762
|
-
warn("API token not set
|
|
762
|
+
warn("API token not set, DNS records and Access app not updated")
|
|
763
763
|
else:
|
|
764
764
|
warn("No domain entered")
|
|
765
765
|
|
|
@@ -902,7 +902,7 @@ def cmd_build(env_file: Path, images: list[str] | None, confirm: bool = True) ->
|
|
|
902
902
|
running = [line.strip() for line in out.strip().splitlines() if line.strip()]
|
|
903
903
|
if running:
|
|
904
904
|
warn_header(
|
|
905
|
-
"Restart required to apply new images
|
|
905
|
+
"Restart required to apply new images. Studio will be briefly unavailable"
|
|
906
906
|
)
|
|
907
907
|
info(f"Running services: {', '.join(running)}")
|
|
908
908
|
if _interactive_yn("Restart now?", default=True, nav=False):
|
|
@@ -963,7 +963,7 @@ def cmd_start(context: str, env_file: Path) -> None:
|
|
|
963
963
|
run(compose_cmd(env_file) + ["pull"], timeout=600)
|
|
964
964
|
except Exception:
|
|
965
965
|
error("The download was interrupted before it finished.")
|
|
966
|
-
warn("Run Start again
|
|
966
|
+
warn("Run Start again, it should pick up where it left off.")
|
|
967
967
|
return False
|
|
968
968
|
ok("Images pulled")
|
|
969
969
|
|
|
@@ -1077,7 +1077,7 @@ def _core_plan(
|
|
|
1077
1077
|
container: str, pg_container: str, nginx_port: int | str = 80
|
|
1078
1078
|
) -> "_BootstrapPlan":
|
|
1079
1079
|
# Core's API runs in `container`, but Postgres is an external sidecar
|
|
1080
|
-
# (`pg_container`)
|
|
1080
|
+
# (`pg_container`), so password hashing execs into the API container while
|
|
1081
1081
|
# every psql runs against the sidecar. base omits "exec"; helpers append it.
|
|
1082
1082
|
# Core launches with publish_internal=False, so port 8000 is not published:
|
|
1083
1083
|
# reach the API through the front door.
|
|
@@ -1134,7 +1134,7 @@ def _unset_env_for_bootstrap(
|
|
|
1134
1134
|
|
|
1135
1135
|
|
|
1136
1136
|
def _super_admin_exists(plan: "_BootstrapPlan", env_data: dict) -> bool:
|
|
1137
|
-
"""True if a super_admin user is already in the DB
|
|
1137
|
+
"""True if a super_admin user is already in the DB, the bootstrap gate."""
|
|
1138
1138
|
pg_user = env_data.get("POSTGRES_USER", "postgres")
|
|
1139
1139
|
rc, out = run_quiet(
|
|
1140
1140
|
plan.base
|
|
@@ -1251,7 +1251,7 @@ def _bootstrap_first_admin(
|
|
|
1251
1251
|
plan,
|
|
1252
1252
|
)
|
|
1253
1253
|
return True
|
|
1254
|
-
warn("Bootstrap incomplete
|
|
1254
|
+
warn("Bootstrap incomplete, will retry account creation on next start.")
|
|
1255
1255
|
return False
|
|
1256
1256
|
|
|
1257
1257
|
|
|
@@ -1574,7 +1574,7 @@ def _app_db_url(env_data: dict) -> str:
|
|
|
1574
1574
|
|
|
1575
1575
|
def _print_db_role_posture(env_data: dict, api_up: bool) -> None:
|
|
1576
1576
|
"""One-line RLS posture. A healthy API with the app URL set proves
|
|
1577
|
-
restricted mode
|
|
1577
|
+
restricted mode, boot is fail-closed on an RLS-inert role."""
|
|
1578
1578
|
if _app_db_url(env_data):
|
|
1579
1579
|
if api_up:
|
|
1580
1580
|
print(
|
|
@@ -1584,12 +1584,12 @@ def _print_db_role_posture(env_data: dict, api_up: bool) -> None:
|
|
|
1584
1584
|
else:
|
|
1585
1585
|
print(
|
|
1586
1586
|
f" {'DB role':24s} {_yellow('restricted (configured)')} "
|
|
1587
|
-
f"{_dim('API down
|
|
1587
|
+
f"{_dim('API down, an RlsInertError in its logs means SHS_DATABASE_APP_URL is misconfigured')}"
|
|
1588
1588
|
)
|
|
1589
1589
|
else:
|
|
1590
1590
|
print(
|
|
1591
1591
|
f" {'DB role':24s} {_yellow('privileged')} "
|
|
1592
|
-
f"{_dim('RLS inert, app-layer checks only
|
|
1592
|
+
f"{_dim('RLS inert, app-layer checks only, enable via the DB role menu')}"
|
|
1593
1593
|
)
|
|
1594
1594
|
|
|
1595
1595
|
|
|
@@ -1864,7 +1864,7 @@ def cmd_reset_password(context: str, env_file: Path) -> None:
|
|
|
1864
1864
|
info("Resetting admin password...")
|
|
1865
1865
|
try:
|
|
1866
1866
|
# SHS_ names are the app script's env contract (scripts/reset_admin_password.py
|
|
1867
|
-
# reads SHS_ADMIN_PASSWORD / SHS_FORCE_PRODUCTION)
|
|
1867
|
+
# reads SHS_ADMIN_PASSWORD / SHS_FORCE_PRODUCTION), keep them, do NOT rename
|
|
1868
1868
|
# to CONSOLE_. Transient injection only; never written to .env.
|
|
1869
1869
|
run(
|
|
1870
1870
|
compose_cmd(env_file)
|
|
@@ -1884,7 +1884,7 @@ def cmd_reset_password(context: str, env_file: Path) -> None:
|
|
|
1884
1884
|
error("Failed to reset password. Check that the API container is healthy.")
|
|
1885
1885
|
else:
|
|
1886
1886
|
info("Resetting admin password...")
|
|
1887
|
-
# SHS_ names are the app script's env contract
|
|
1887
|
+
# SHS_ names are the app script's env contract, keep, do NOT rename to CONSOLE_.
|
|
1888
1888
|
os.environ["SHS_ADMIN_PASSWORD"] = new_password
|
|
1889
1889
|
os.environ["SHS_FORCE_PRODUCTION"] = "true"
|
|
1890
1890
|
try:
|
|
@@ -2039,7 +2039,7 @@ def _format_local_time(iso_string: str) -> tuple[str, str]:
|
|
|
2039
2039
|
def _verify_post_restore(context: str, env_file: Path, db_file: str) -> None:
|
|
2040
2040
|
"""After restore, confirm the live DB's alembic_version matches the dump's.
|
|
2041
2041
|
|
|
2042
|
-
Mismatch means the apply succeeded but landed on the wrong schema
|
|
2042
|
+
Mismatch means the apply succeeded but landed on the wrong schema, surface
|
|
2043
2043
|
it loudly so the operator doesn't trust a silent success.
|
|
2044
2044
|
"""
|
|
2045
2045
|
expected = _read_revision_from_dump(db_file)
|
|
@@ -2111,7 +2111,7 @@ def _restore_preflight(context: str, env_file: Path, db_file: str) -> bool:
|
|
|
2111
2111
|
|
|
2112
2112
|
# Encryption-key check: the dump's encrypted columns are only readable with
|
|
2113
2113
|
# the key they were written under. We never store that key in the backup, so
|
|
2114
|
-
# we compare fingerprints
|
|
2114
|
+
# we compare fingerprints, live key vs. the fp recorded in the header.
|
|
2115
2115
|
backup_key_fp = header.get("encryption_key_fp", "")
|
|
2116
2116
|
live_key_fp = _encryption_key_fingerprint(env_data)
|
|
2117
2117
|
key_mismatch = bool(backup_key_fp and live_key_fp and backup_key_fp != live_key_fp)
|
|
@@ -2172,7 +2172,7 @@ def _restore_preflight(context: str, env_file: Path, db_file: str) -> bool:
|
|
|
2172
2172
|
)
|
|
2173
2173
|
if _offer_key_recovery(env_file, backup_key_fp):
|
|
2174
2174
|
return True # key now matches; fall through to restore
|
|
2175
|
-
info(_cyan("Restore needs the matching key
|
|
2175
|
+
info(_cyan("Restore needs the matching key. The data is unreadable without it."))
|
|
2176
2176
|
info("Aborted (no valid encryption key).")
|
|
2177
2177
|
return False
|
|
2178
2178
|
|
|
@@ -2259,7 +2259,7 @@ def cmd_backup(
|
|
|
2259
2259
|
|
|
2260
2260
|
ok(f"Backup complete: {backup_dir}")
|
|
2261
2261
|
|
|
2262
|
-
# The encryption key is deliberately NOT stored in the backup
|
|
2262
|
+
# The encryption key is deliberately NOT stored in the backup, co-locating
|
|
2263
2263
|
# it with the ciphertext would let anyone holding the backup decrypt it.
|
|
2264
2264
|
# Skip the notice for internal pre-restore snapshots (not operator-facing).
|
|
2265
2265
|
if what in ("all", "db") and name_prefix != "pre-restore":
|
|
@@ -2275,7 +2275,7 @@ def _print_encryption_key_notice(env_data: dict) -> None:
|
|
|
2275
2275
|
print(_bold("⚠ Record your encryption key separately"))
|
|
2276
2276
|
print("─" * 40)
|
|
2277
2277
|
if key:
|
|
2278
|
-
print("This backup does NOT contain your encryption key
|
|
2278
|
+
print("This backup does NOT contain your encryption key, by design, so a")
|
|
2279
2279
|
print("stolen backup can't be decrypted. The dump is unrecoverable without it.")
|
|
2280
2280
|
print()
|
|
2281
2281
|
print("Copy SHS_CREDENTIAL_ENCRYPTION_KEY from your .env into a password manager or")
|
|
@@ -2283,7 +2283,7 @@ def _print_encryption_key_notice(env_data: dict) -> None:
|
|
|
2283
2283
|
print()
|
|
2284
2284
|
print(f" SHS_CREDENTIAL_ENCRYPTION_KEY={key}")
|
|
2285
2285
|
else:
|
|
2286
|
-
print(_yellow("No SHS_CREDENTIAL_ENCRYPTION_KEY is set in .env
|
|
2286
|
+
print(_yellow("No SHS_CREDENTIAL_ENCRYPTION_KEY is set in .env. Nothing to record."))
|
|
2287
2287
|
print()
|
|
2288
2288
|
|
|
2289
2289
|
|
|
@@ -2416,7 +2416,7 @@ def cmd_restore_db(
|
|
|
2416
2416
|
|
|
2417
2417
|
try:
|
|
2418
2418
|
if context == "host":
|
|
2419
|
-
# Stop API/UI before dropping the schema
|
|
2419
|
+
# Stop API/UI before dropping the schema, a live API reconnects
|
|
2420
2420
|
# to postgres mid-restore (pg_terminate_backend only kills current
|
|
2421
2421
|
# connections) and reads/writes a half-restored schema. The
|
|
2422
2422
|
# pre-restore snapshot above is the recovery net if this fails.
|
|
@@ -2512,10 +2512,10 @@ def cmd_restore_db(
|
|
|
2512
2512
|
|
|
2513
2513
|
ok("Database restored")
|
|
2514
2514
|
_verify_post_restore(context, env_file, db_file)
|
|
2515
|
-
# .env (and its encryption key) is intentionally left untouched
|
|
2515
|
+
# .env (and its encryption key) is intentionally left untouched, the key
|
|
2516
2516
|
# is never stored in the backup. The preflight already gated key mismatch.
|
|
2517
2517
|
|
|
2518
|
-
# API/UI were stopped for exclusive DB access
|
|
2518
|
+
# API/UI were stopped for exclusive DB access, bring them back up.
|
|
2519
2519
|
if context == "host":
|
|
2520
2520
|
info("Restarting API/UI...")
|
|
2521
2521
|
env_data2 = read_env(env_file)
|
|
@@ -2533,14 +2533,14 @@ def cmd_restore_db(
|
|
|
2533
2533
|
)
|
|
2534
2534
|
else:
|
|
2535
2535
|
# Provisioning/grants/RLS re-apply in the container ENTRYPOINT, so the
|
|
2536
|
-
# restore flow ends with a container restart
|
|
2536
|
+
# restore flow ends with a container restart, supervisorctl restart
|
|
2537
2537
|
# is not enough. Until then the restricted role authenticates but every
|
|
2538
2538
|
# table access is permission-denied → sanitized 500s (fail-closed).
|
|
2539
2539
|
warn("Restart this container from the host now to finish the restore.")
|
|
2540
2540
|
print(f" {_bold('docker restart <container>')} {_dim('(or stop/start the pod on RunPod)')}")
|
|
2541
2541
|
if _app_db_url(env_data):
|
|
2542
2542
|
warn(
|
|
2543
|
-
"Until the restart, API requests fail with sanitized 500s
|
|
2543
|
+
"Until the restart, API requests fail with sanitized 500s, "
|
|
2544
2544
|
"the restore dropped the restricted role's grants; boot re-applies them."
|
|
2545
2545
|
)
|
|
2546
2546
|
|
|
@@ -2579,7 +2579,7 @@ def cmd_restore(
|
|
|
2579
2579
|
db_name = "selfhost_studio"
|
|
2580
2580
|
|
|
2581
2581
|
if context == "host":
|
|
2582
|
-
# Stop API/UI before dropping the schema
|
|
2582
|
+
# Stop API/UI before dropping the schema, see cmd_restore_db.
|
|
2583
2583
|
# A live API reconnects mid-restore and reads a half-restored
|
|
2584
2584
|
# schema; the pre-restore snapshot above is the recovery net.
|
|
2585
2585
|
info("Stopping API/UI for exclusive DB access...")
|
|
@@ -2668,7 +2668,7 @@ def cmd_restore(
|
|
|
2668
2668
|
)
|
|
2669
2669
|
ok("Database restored")
|
|
2670
2670
|
_verify_post_restore(context, env_file, db_file)
|
|
2671
|
-
# .env (and its encryption key) is intentionally left untouched
|
|
2671
|
+
# .env (and its encryption key) is intentionally left untouched, the
|
|
2672
2672
|
# key is never stored in the backup. Preflight gated key mismatch.
|
|
2673
2673
|
else:
|
|
2674
2674
|
warn(f"No database.sql in {restore_dir}")
|
|
@@ -2684,7 +2684,7 @@ def cmd_restore(
|
|
|
2684
2684
|
else:
|
|
2685
2685
|
warn(f"No orgs.tar.gz in {restore_dir}")
|
|
2686
2686
|
|
|
2687
|
-
# API/UI were stopped for exclusive DB access
|
|
2687
|
+
# API/UI were stopped for exclusive DB access, bring them back up.
|
|
2688
2688
|
if context == "host":
|
|
2689
2689
|
print()
|
|
2690
2690
|
info("Restarting API/UI...")
|
|
@@ -2812,7 +2812,7 @@ def cmd_upgrade(context: str, env_file: Path) -> None:
|
|
|
2812
2812
|
run(compose_cmd(env_file) + ["pull"], timeout=300)
|
|
2813
2813
|
ok("Images pulled")
|
|
2814
2814
|
|
|
2815
|
-
# Restart
|
|
2815
|
+
# Restart, reapply scale flags + --remove-orphans so replica counts survive
|
|
2816
2816
|
# the upgrade and stale containers from the old version are cleaned up.
|
|
2817
2817
|
info("Restarting services...")
|
|
2818
2818
|
env_data = read_env(env_file)
|
|
@@ -2870,7 +2870,7 @@ def _detect_install_method() -> str:
|
|
|
2870
2870
|
if marker.exists():
|
|
2871
2871
|
return marker.read_text().strip()
|
|
2872
2872
|
|
|
2873
|
-
# uv tool install
|
|
2873
|
+
# uv tool install, path contains uv/tools, no pip module available
|
|
2874
2874
|
if "uv/tools" in str(pkg_dir).replace("\\", "/"):
|
|
2875
2875
|
return "uv"
|
|
2876
2876
|
|
|
@@ -2882,7 +2882,7 @@ def _detect_install_method() -> str:
|
|
|
2882
2882
|
if "Cellar" in str(pkg_dir) or "homebrew" in str(pkg_dir).lower():
|
|
2883
2883
|
return "brew"
|
|
2884
2884
|
|
|
2885
|
-
# Inside a git repo
|
|
2885
|
+
# Inside a git repo, dev mode
|
|
2886
2886
|
if (pkg_dir / ".git").exists() or (pkg_dir.parent / ".git").exists():
|
|
2887
2887
|
return "dev"
|
|
2888
2888
|
|
|
@@ -2890,7 +2890,7 @@ def _detect_install_method() -> str:
|
|
|
2890
2890
|
|
|
2891
2891
|
|
|
2892
2892
|
def cmd_self_update(context: str) -> None:
|
|
2893
|
-
"""Update studio-console
|
|
2893
|
+
"""Update studio-console, delegates to the correct mechanism for the install method."""
|
|
2894
2894
|
import json
|
|
2895
2895
|
import os
|
|
2896
2896
|
import shutil
|
|
@@ -2905,14 +2905,14 @@ def cmd_self_update(context: str) -> None:
|
|
|
2905
2905
|
|
|
2906
2906
|
if context in ("container", "runpod"):
|
|
2907
2907
|
warn("studio-console is baked into the container image.")
|
|
2908
|
-
warn("Update by pulling a new image tag
|
|
2908
|
+
warn("Update by pulling a new image tag, not via self-update.")
|
|
2909
2909
|
return
|
|
2910
2910
|
|
|
2911
2911
|
method = _detect_install_method()
|
|
2912
2912
|
info(f"Current version: {__version__} (installed via {method})")
|
|
2913
2913
|
|
|
2914
2914
|
if method == "dev":
|
|
2915
|
-
warn("Running from source
|
|
2915
|
+
warn("Running from source. Use git pull to update.")
|
|
2916
2916
|
return
|
|
2917
2917
|
|
|
2918
2918
|
if method == "brew":
|
|
@@ -2927,10 +2927,16 @@ def cmd_self_update(context: str) -> None:
|
|
|
2927
2927
|
[os.sys.executable, "-m", "pip", "install", "--upgrade", "studio-console"],
|
|
2928
2928
|
timeout=120,
|
|
2929
2929
|
)
|
|
2930
|
-
ok("Updated
|
|
2930
|
+
ok("Updated. Restart studio-console to use the new version.")
|
|
2931
2931
|
return
|
|
2932
2932
|
|
|
2933
|
-
|
|
2933
|
+
if method == "uv":
|
|
2934
|
+
info("Updating via uv tool...")
|
|
2935
|
+
run(["uv", "tool", "install", "--force", "studio-console"], timeout=120)
|
|
2936
|
+
ok("Updated. Restart studio-console to use the new version.")
|
|
2937
|
+
return
|
|
2938
|
+
|
|
2939
|
+
# curl installs resolve the latest GitHub release and extract its tarball
|
|
2934
2940
|
info("Checking for updates...")
|
|
2935
2941
|
api_url = "https://api.github.com/repos/selfhosthub/studio-console/releases/latest"
|
|
2936
2942
|
try:
|
|
@@ -2953,16 +2959,6 @@ def cmd_self_update(context: str) -> None:
|
|
|
2953
2959
|
|
|
2954
2960
|
info(f"Updating {__version__} → {latest_version}")
|
|
2955
2961
|
|
|
2956
|
-
if method == "uv":
|
|
2957
|
-
wheel_url = (
|
|
2958
|
-
f"https://github.com/selfhosthub/studio-console/releases/download/"
|
|
2959
|
-
f"{latest_tag}/studio_console-{latest_version}-py3-none-any.whl"
|
|
2960
|
-
)
|
|
2961
|
-
info("Updating via uv tool...")
|
|
2962
|
-
run(["uv", "tool", "install", "--force", wheel_url], timeout=120)
|
|
2963
|
-
ok("Updated — restart studio-console to use the new version")
|
|
2964
|
-
return
|
|
2965
|
-
|
|
2966
2962
|
tarball_url = f"https://github.com/selfhosthub/studio-console/archive/refs/tags/{latest_tag}.tar.gz"
|
|
2967
2963
|
install_dir = Path(__file__).resolve().parent.parent
|
|
2968
2964
|
|
studio_console-1.3.4/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.3.4
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|