Skip to content

Commit a02db20

Browse files
feat(theme): adding theme
1 parent 80809f6 commit a02db20

5 files changed

Lines changed: 320 additions & 2 deletions

File tree

Dockerfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
55
PIP_NO_CACHE_DIR=1 \
66
PIP_DISABLE_PIP_VERSION_CHECK=1 \
77
DEVPI_SERVERDIR=/data/server \
8-
DEVPI_SECRETFILE=/data/.secret
8+
DEVPI_SECRETFILE=/data/.secret \
9+
DEVPI_THEME_DIR=/app/theme
910

1011
RUN apt-get update \
1112
&& apt-get install -y --no-install-recommends curl ca-certificates \
@@ -16,6 +17,7 @@ RUN pip install \
1617
"devpi-web>=5.0,<6" \
1718
"devpi-client>=7.0,<8"
1819

20+
COPY theme /app/theme
1921
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
2022
RUN chmod +x /usr/local/bin/entrypoint.sh
2123

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ Based on [devpi](https://devpi.net), with persistent storage and full compatibil
3030

3131
## Use it
3232

33+
> **The web UI is browse-only.** devpi-web shows your indexes, packages, and search — but there's **no web login form**. All authenticated actions (logging in, creating users, creating indexes, uploading packages) go through the `devpi-client` CLI described below. This is by design in devpi.
34+
3335
### Configure the devpi client
3436

3537
```bash
@@ -40,15 +42,24 @@ devpi login root --password="$DEVPI_ROOT_PASSWORD"
4042

4143
### Create a user and a private index
4244

45+
By default the server runs with `--restrict-modify root`, so only `root` can create users and indexes. While still logged in as `root`:
46+
4347
```bash
4448
devpi user -c alice password=s3cret email=alice@example.com
49+
devpi index -c alice/prod bases=root/pypi
50+
```
51+
52+
Then switch to the new user to publish:
53+
54+
```bash
4555
devpi login alice --password=s3cret
46-
devpi index -c prod bases=root/pypi
4756
devpi use alice/prod
4857
```
4958

5059
`bases=root/pypi` makes your index fall through to the on-demand PyPI mirror, so `pip install`ing a public dependency through your index Just Works.
5160

61+
> Want any authenticated user to create their own indexes? Set the service variable `DEVPI_RESTRICT_MODIFY=""` and redeploy.
62+
5263
### Publish a package
5364

5465
With `devpi upload` (builds + uploads from the current project):
@@ -95,6 +106,7 @@ For CI, prefer a scoped token over a password: see [`devpi-tokens`](https://pypi
95106
| `DEVPI_SERVERDIR` | `/data/server` | Where devpi stores packages, indexes, and metadata. Must be on the mounted volume. |
96107
| `DEVPI_SECRETFILE` | `/data/.secret` | Persistent server secret. Required so login tokens survive a redeploy. |
97108
| `DEVPI_RESTRICT_MODIFY` | `root` | Only `root` can create users/indexes. Set to an empty string to allow any logged-in user to create their own. |
109+
| `DEVPI_THEME_DIR` | `/app/theme` | Folder passed to devpi-server's `--theme`. Ships with a built-in modern theme (system fonts, dark mode). Point it at a folder under `/data` to use your own, or set to an empty string for the stock devpi look. |
98110

99111
## Persistence & the `/data` volume
100112

entrypoint.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,12 @@ if [ -n "$DEVPI_RESTRICT_MODIFY" ]; then
4141
server_args+=(--restrict-modify "$DEVPI_RESTRICT_MODIFY")
4242
fi
4343

44+
if [ -n "${DEVPI_THEME_DIR:-}" ] && [ -d "$DEVPI_THEME_DIR" ]; then
45+
echo "[entrypoint] using devpi-web theme at $DEVPI_THEME_DIR"
46+
server_args+=(--theme "$DEVPI_THEME_DIR")
47+
elif [ -n "${DEVPI_THEME_DIR:-}" ]; then
48+
echo "[entrypoint] warning: DEVPI_THEME_DIR=$DEVPI_THEME_DIR is set but not a directory, skipping theme"
49+
fi
50+
4451
echo "[entrypoint] starting devpi-server on 0.0.0.0:$PORT"
4552
exec devpi-server "${server_args[@]}"

scripts/smoke-test.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,21 @@ pre_clean
9292
docker volume create "$VOLUME_NAME" >/dev/null
9393
start_container
9494

95+
# --- 3b. verify the custom theme CSS is actually served ------------------
96+
log "checking that the custom theme CSS is referenced and served"
97+
ROOT_HTML="$(curl -fsS "http://localhost:${HOST_PORT}/")"
98+
THEME_URL="$(printf '%s\n' "$ROOT_HTML" | grep -oE '\+theme-static-[^"]+style\.css' | head -1 || true)"
99+
if [ -z "$THEME_URL" ]; then
100+
printf '%s\n' "$ROOT_HTML" | head -40
101+
fail "root page does not reference a +theme-static-*/style.css link — --theme flag not picked up"
102+
fi
103+
THEME_CSS_STATUS="$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:${HOST_PORT}/${THEME_URL}")"
104+
if [ "$THEME_CSS_STATUS" != "200" ]; then
105+
docker logs "$CONTAINER_NAME" | tail -40 || true
106+
fail "theme CSS at /${THEME_URL} did not return 200 (got: $THEME_CSS_STATUS)"
107+
fi
108+
log "theme CSS served at /${THEME_URL}"
109+
95110
# --- 4. create user + index, upload wheel --------------------------------
96111
log "configuring devpi client inside container"
97112
in_container devpi use "http://localhost:3141"

theme/static/style.css

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
:root {
2+
--bg: #fafafa;
3+
--surface: #ffffff;
4+
--border: #e5e7eb;
5+
--text: #111827;
6+
--text-muted: #6b7280;
7+
--primary: #2563eb;
8+
--primary-hover: #1d4ed8;
9+
--accent: #1e40af;
10+
--success: #15803d;
11+
--warning: #b45309;
12+
--danger: #b91c1c;
13+
--radius: 8px;
14+
--shadow: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.06);
15+
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
16+
}
17+
18+
@media (prefers-color-scheme: dark) {
19+
:root {
20+
--bg: #0b0f17;
21+
--surface: #111827;
22+
--border: #1f2937;
23+
--text: #e5e7eb;
24+
--text-muted: #9ca3af;
25+
--primary: #60a5fa;
26+
--primary-hover: #93c5fd;
27+
--accent: #93c5fd;
28+
--success: #4ade80;
29+
--warning: #fbbf24;
30+
--danger: #f87171;
31+
--shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.3);
32+
}
33+
}
34+
35+
*, *::before, *::after { box-sizing: border-box; }
36+
37+
html { -webkit-text-size-adjust: 100%; }
38+
39+
body {
40+
margin: 0;
41+
padding: 2rem 1.25rem 4rem;
42+
background: var(--bg);
43+
color: var(--text);
44+
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
45+
font-size: 15px;
46+
line-height: 1.55;
47+
-webkit-font-smoothing: antialiased;
48+
-moz-osx-font-smoothing: grayscale;
49+
}
50+
51+
body > * {
52+
max-width: 1100px;
53+
margin-left: auto;
54+
margin-right: auto;
55+
}
56+
57+
h1, h2, h3, h4 {
58+
color: var(--text);
59+
font-weight: 650;
60+
line-height: 1.25;
61+
margin-top: 1.75rem;
62+
margin-bottom: 0.75rem;
63+
text-decoration: none;
64+
}
65+
66+
h1 {
67+
font-size: 1.9rem;
68+
letter-spacing: -0.01em;
69+
border-bottom: 1px solid var(--border);
70+
padding-bottom: 0.6rem;
71+
margin-top: 0;
72+
}
73+
74+
h1 a, h1 a:visited {
75+
color: var(--text);
76+
text-decoration: none;
77+
}
78+
79+
h2 { font-size: 1.35rem; }
80+
h3 { font-size: 1.1rem; color: var(--text-muted); }
81+
82+
a {
83+
color: var(--primary);
84+
text-decoration: none;
85+
transition: color 0.12s ease;
86+
}
87+
88+
a:hover { color: var(--primary-hover); text-decoration: underline; }
89+
a:visited { color: var(--accent); }
90+
91+
code, pre, tt {
92+
font-family: var(--mono);
93+
font-size: 0.9em;
94+
}
95+
96+
code, tt {
97+
background: var(--surface);
98+
border: 1px solid var(--border);
99+
border-radius: 4px;
100+
padding: 0.1em 0.4em;
101+
}
102+
103+
pre {
104+
background: var(--surface);
105+
border: 1px solid var(--border);
106+
border-radius: var(--radius);
107+
padding: 1rem;
108+
overflow-x: auto;
109+
box-shadow: var(--shadow);
110+
}
111+
112+
pre code { background: transparent; border: none; padding: 0; }
113+
114+
input[type="text"], input[type="search"], input[type="password"] {
115+
background: var(--surface);
116+
color: var(--text);
117+
border: 1px solid var(--border);
118+
border-radius: var(--radius);
119+
padding: 0.6rem 0.85rem;
120+
font-size: 0.95rem;
121+
font-family: inherit;
122+
min-width: 260px;
123+
box-shadow: var(--shadow);
124+
transition: border-color 0.12s ease, box-shadow 0.12s ease;
125+
}
126+
127+
input[type="text"]:focus, input[type="search"]:focus, input[type="password"]:focus {
128+
outline: none;
129+
border-color: var(--primary);
130+
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
131+
}
132+
133+
button, input[type="submit"], input[type="button"] {
134+
background: var(--primary);
135+
color: #fff;
136+
border: none;
137+
border-radius: var(--radius);
138+
padding: 0.6rem 1.1rem;
139+
font-size: 0.95rem;
140+
font-family: inherit;
141+
font-weight: 550;
142+
cursor: pointer;
143+
transition: background 0.12s ease, transform 0.06s ease;
144+
box-shadow: var(--shadow);
145+
}
146+
147+
button:hover, input[type="submit"]:hover, input[type="button"]:hover {
148+
background: var(--primary-hover);
149+
}
150+
151+
button:active, input[type="submit"]:active, input[type="button"]:active {
152+
transform: translateY(1px);
153+
}
154+
155+
form { margin: 1rem 0; }
156+
157+
form input[type="text"], form input[type="search"] { margin-right: 0.5rem; }
158+
159+
ul, ol {
160+
padding-left: 1.4rem;
161+
margin: 0.6rem 0 1.2rem;
162+
}
163+
164+
li { margin: 0.25rem 0; }
165+
166+
.projectlist, .indexlist, ul.projects, ul.indexes {
167+
list-style: none;
168+
padding: 0;
169+
display: grid;
170+
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
171+
gap: 0.5rem;
172+
}
173+
174+
.projectlist li, .indexlist li, ul.projects li, ul.indexes li {
175+
background: var(--surface);
176+
border: 1px solid var(--border);
177+
border-radius: var(--radius);
178+
padding: 0.6rem 0.9rem;
179+
box-shadow: var(--shadow);
180+
margin: 0;
181+
}
182+
183+
table {
184+
width: 100%;
185+
border-collapse: collapse;
186+
background: var(--surface);
187+
border: 1px solid var(--border);
188+
border-radius: var(--radius);
189+
overflow: hidden;
190+
margin: 1rem 0;
191+
box-shadow: var(--shadow);
192+
}
193+
194+
th, td {
195+
padding: 0.65rem 0.9rem;
196+
text-align: left;
197+
border-bottom: 1px solid var(--border);
198+
font-size: 0.92rem;
199+
}
200+
201+
th {
202+
background: var(--bg);
203+
font-weight: 600;
204+
color: var(--text-muted);
205+
text-transform: uppercase;
206+
letter-spacing: 0.03em;
207+
font-size: 0.78rem;
208+
}
209+
210+
tr:last-child td { border-bottom: none; }
211+
212+
hr {
213+
border: none;
214+
border-top: 1px solid var(--border);
215+
margin: 1.5rem 0;
216+
}
217+
218+
.status-badge, .badge, .status {
219+
display: inline-block;
220+
padding: 0.15rem 0.55rem;
221+
border-radius: 999px;
222+
font-size: 0.78rem;
223+
font-weight: 600;
224+
text-transform: uppercase;
225+
letter-spacing: 0.02em;
226+
background: var(--surface);
227+
border: 1px solid var(--border);
228+
color: var(--text-muted);
229+
}
230+
231+
.status-ok, .badge-ok, .status.ok { color: var(--success); border-color: var(--success); }
232+
.status-degraded, .status.degraded, .badge-warning { color: var(--warning); border-color: var(--warning); }
233+
.status-failed, .status.failed, .badge-error { color: var(--danger); border-color: var(--danger); }
234+
235+
.searchbar, .search, #search {
236+
background: var(--surface);
237+
border: 1px solid var(--border);
238+
border-radius: var(--radius);
239+
padding: 1rem;
240+
margin: 1rem 0 1.5rem;
241+
box-shadow: var(--shadow);
242+
display: flex;
243+
gap: 0.5rem;
244+
flex-wrap: wrap;
245+
align-items: center;
246+
}
247+
248+
.searchbar input[type="text"], .search input[type="text"] {
249+
flex: 1 1 300px;
250+
min-width: 200px;
251+
}
252+
253+
.breadcrumbs, .breadcrumb {
254+
color: var(--text-muted);
255+
font-size: 0.9rem;
256+
margin: 0.25rem 0 1rem;
257+
}
258+
259+
.breadcrumbs a, .breadcrumb a { color: var(--text-muted); }
260+
.breadcrumbs a:hover, .breadcrumb a:hover { color: var(--primary); }
261+
262+
footer, .footer {
263+
margin-top: 3rem;
264+
padding-top: 1rem;
265+
border-top: 1px solid var(--border);
266+
color: var(--text-muted);
267+
font-size: 0.82rem;
268+
text-align: center;
269+
}
270+
271+
footer a, .footer a { color: var(--text-muted); }
272+
273+
.how-to-search, a[href*="how-to"], a[title*="search"] {
274+
font-size: 0.85rem;
275+
color: var(--text-muted);
276+
}
277+
278+
@media (max-width: 640px) {
279+
body { padding: 1.25rem 0.9rem 3rem; font-size: 14px; }
280+
h1 { font-size: 1.55rem; }
281+
.projectlist, .indexlist, ul.projects, ul.indexes { grid-template-columns: 1fr; }
282+
}

0 commit comments

Comments
 (0)