landfall 0.4.6 → 0.5.1
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/README.md +90 -41
- package/dist/cli.mjs +234 -210
- package/dist/index.html +182 -164
- package/dist/server-DA-e5nav.mjs +9 -0
- package/package.json +2 -2
- package/dist/server-c9LxeAd1.mjs +0 -5
package/dist/index.html
CHANGED
|
@@ -9,171 +9,179 @@
|
|
|
9
9
|
:root {
|
|
10
10
|
/* Three text sizes and three corner radii; headings set their own. */
|
|
11
11
|
--fs-xs: 11.5px; --fs-sm: 12.5px; --fs: 13px;
|
|
12
|
-
--r-sm:
|
|
13
|
-
--bg: #
|
|
14
|
-
--accent: #
|
|
15
|
-
--bad: #
|
|
12
|
+
--r-sm: 7px; --r: 10px; --r-lg: 14px;
|
|
13
|
+
--bg: #fafafa; --panel: #fff; --hover: #f4f4f5; --line: #ececee; --line-strong: #d4d4d8; --text: #18181b; --dim: #5d5d66; --faint: #71717a;
|
|
14
|
+
--accent: #4f46e5; --accent-bg: #eef0fe; --accent-solid: #4f46e5; --bad-solid: #dc2626; --ok: #15803d; --ok-bg: #eaf7ee; --warn: #b45309; --warn-bg: #fdf4e3;
|
|
15
|
+
--bad: #c42323; --bad-bg: #fcecec; --term: #0f0f12; --term-text: #dcdce1;
|
|
16
|
+
--shadow: 0 1px 2px rgb(0 0 0 / .04); --shadow-lg: 0 10px 30px -8px rgb(0 0 0 / .14), 0 2px 6px rgb(0 0 0 / .05);
|
|
17
|
+
--ring: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
|
|
16
18
|
--mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
|
|
17
19
|
color-scheme: light;
|
|
18
20
|
}
|
|
19
21
|
@media (prefers-color-scheme: dark) {
|
|
20
22
|
:root {
|
|
21
|
-
--bg: #
|
|
22
|
-
--accent: #
|
|
23
|
-
--bad: #
|
|
23
|
+
--bg: #0c0c0e; --panel: #141417; --hover: #1c1c20; --line: #232328; --line-strong: #34343b; --text: #ececef; --dim: #a1a1aa; --faint: #8b8b94;
|
|
24
|
+
--accent: #8b93ff; --accent-bg: #1d1d3a; --accent-solid: #5b54e8; --bad-solid: #d33a3a; --ok: #4ade80; --ok-bg: #122a1b; --warn: #f5b34a; --warn-bg: #2b2212;
|
|
25
|
+
--bad: #ff7b7b; --bad-bg: #321819; --term: #08080a; --term-text: #dcdce1;
|
|
26
|
+
--shadow: 0 1px 2px rgb(0 0 0 / .4); --shadow-lg: 0 16px 40px -8px rgb(0 0 0 / .6), 0 2px 6px rgb(0 0 0 / .4);
|
|
24
27
|
color-scheme: dark;
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
30
|
* { box-sizing: border-box; }
|
|
28
31
|
html, body { margin: 0; height: 100%; }
|
|
29
|
-
body { background: var(--bg); color: var(--text); font: 14px/1.
|
|
32
|
+
body { background: var(--bg); color: var(--text); font: 14px/1.5 Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; -webkit-font-smoothing: antialiased; }
|
|
30
33
|
a { color: var(--accent); text-decoration: none; }
|
|
31
|
-
a:hover { text-decoration: underline; }
|
|
34
|
+
a:hover { text-decoration: underline; text-underline-offset: 2px; }
|
|
32
35
|
code, .mono { font-family: var(--mono); font-size: var(--fs-sm); }
|
|
33
36
|
button {
|
|
34
|
-
font: inherit; font-size: var(--fs); border: 1px solid var(--line); background: var(--panel); color: var(--text);
|
|
35
|
-
padding: 5px
|
|
37
|
+
font: inherit; font-size: var(--fs); font-weight: 500; border: 1px solid var(--line-strong); background: var(--panel); color: var(--text);
|
|
38
|
+
padding: 5px 12px; border-radius: var(--r-sm); cursor: pointer; white-space: nowrap; box-shadow: var(--shadow);
|
|
39
|
+
transition: background-color .12s, border-color .12s, color .12s, box-shadow .12s;
|
|
36
40
|
}
|
|
37
|
-
button:hover {
|
|
38
|
-
button:focus-visible, a:focus-visible { outline:
|
|
39
|
-
button.primary:hover { filter: brightness(1.08); }
|
|
41
|
+
button:hover { background: var(--hover); }
|
|
42
|
+
button:focus-visible, a:focus-visible { outline: none; box-shadow: var(--ring); border-color: var(--accent); }
|
|
40
43
|
button:disabled { opacity: .5; cursor: default; }
|
|
41
44
|
button.primary { background: var(--accent-solid); border-color: var(--accent-solid); color: #fff; }
|
|
45
|
+
button.primary:hover, button.danger.solid:hover { filter: brightness(1.1); }
|
|
42
46
|
button.danger { color: var(--bad); }
|
|
47
|
+
button.danger:hover { background: var(--bad-bg); }
|
|
43
48
|
button.danger.solid { background: var(--bad-solid); border-color: var(--bad-solid); color: #fff; }
|
|
44
|
-
button.
|
|
45
|
-
button.ghost {
|
|
46
|
-
button.
|
|
47
|
-
button.sm { padding: 2px 8px; font-size: var(--fs-sm); }
|
|
49
|
+
button.ghost { border-color: transparent; background: transparent; color: var(--dim); box-shadow: none; }
|
|
50
|
+
button.ghost:hover { color: var(--text); background: var(--hover); }
|
|
51
|
+
button.sm { padding: 2px 9px; font-size: var(--fs-sm); }
|
|
48
52
|
input, select, textarea {
|
|
49
|
-
font: inherit; color: var(--text); background: var(--panel); border: 1px solid var(--line-strong); border-radius: var(--r-sm); padding:
|
|
53
|
+
font: inherit; color: var(--text); background: var(--panel); border: 1px solid var(--line-strong); border-radius: var(--r-sm); padding: 7px 10px; width: 100%;
|
|
54
|
+
transition: border-color .12s, box-shadow .12s;
|
|
50
55
|
}
|
|
51
56
|
input.secret { -webkit-text-security: disc; text-security: disc; }
|
|
52
|
-
input:focus, select:focus, textarea:focus { outline:
|
|
57
|
+
input:focus, select:focus, textarea:focus { outline: none; box-shadow: var(--ring); border-color: var(--accent); }
|
|
53
58
|
textarea { font-family: var(--mono); font-size: var(--fs-sm); min-height: 90px; }
|
|
54
59
|
|
|
55
|
-
.app { display: grid; grid-template-columns:
|
|
56
|
-
.app.with-job { grid-template-columns:
|
|
57
|
-
header { grid-column: 1 / -1; display: flex; align-items: center; gap: 12px; padding: 0
|
|
58
|
-
.brand { font-weight:
|
|
60
|
+
.app { display: grid; grid-template-columns: 248px minmax(0, 1fr); grid-template-rows: 56px minmax(0, 1fr); height: 100vh; }
|
|
61
|
+
.app.with-job { grid-template-columns: 248px minmax(0, 1fr) clamp(360px, 32vw, 520px); }
|
|
62
|
+
header { grid-column: 1 / -1; display: flex; align-items: center; gap: 12px; padding: 0 20px; border-bottom: 1px solid var(--line); background: var(--panel); }
|
|
63
|
+
.brand { font-weight: 650; font-size: 15px; display: flex; align-items: center; gap: 9px; letter-spacing: -.015em; color: var(--text); }
|
|
59
64
|
.brand:hover { text-decoration: none; }
|
|
60
|
-
.brand svg { width:
|
|
61
|
-
.brand small { font-weight:
|
|
65
|
+
.brand svg { width: 24px; height: 24px; flex: none; }
|
|
66
|
+
.brand small { font-weight: 500; color: var(--faint); font-size: var(--fs-xs); letter-spacing: 0; padding: 1px 7px; border-radius: 99px; background: var(--hover); }
|
|
62
67
|
.project { margin-left: auto; display: flex; align-items: center; gap: 8px; color: var(--dim); font-size: var(--fs); min-width: 0; }
|
|
63
|
-
.project .path { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 36vw; }
|
|
64
|
-
nav.servers { border-right: 1px solid var(--line); background: var(--
|
|
65
|
-
.nav-top { position: sticky; top: 0; z-index: 1; background: var(--
|
|
66
|
-
.nav-top input { padding:
|
|
67
|
-
.nav-top button { flex: none; }
|
|
68
|
-
#navlist { padding: 0 8px
|
|
69
|
-
nav h3 { margin: 10px
|
|
70
|
-
nav h3 span { font-weight: 500;
|
|
71
|
-
.srv { display: block; width: 100%; text-align: left; border: 0; background: none; padding: 6px 10px; border-radius: var(--r-sm); }
|
|
72
|
-
.srv:hover { background: var(--
|
|
73
|
-
.srv.on { background: var(--
|
|
74
|
-
.srv.on .n { color: var(--
|
|
75
|
-
.srv.on .t { color: var(--
|
|
76
|
-
.srv .n { font-weight:
|
|
77
|
-
.srv .t { color: var(--
|
|
68
|
+
.project .path { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 36vw; font-family: var(--mono); font-size: var(--fs-sm); }
|
|
69
|
+
nav.servers { border-right: 1px solid var(--line); background: var(--bg); overflow: auto; display: flex; flex-direction: column; }
|
|
70
|
+
.nav-top { position: sticky; top: 0; z-index: 1; background: var(--bg); display: flex; gap: 6px; padding: 14px 12px 8px; }
|
|
71
|
+
.nav-top input { padding: 6px 10px; font-size: var(--fs); }
|
|
72
|
+
.nav-top button { flex: none; padding-inline: 10px; }
|
|
73
|
+
#navlist { padding: 0 8px 16px; }
|
|
74
|
+
nav h3 { margin: 14px 10px 4px; font-size: var(--fs-xs); color: var(--faint); font-weight: 500; display: flex; justify-content: space-between; }
|
|
75
|
+
nav h3 span { font-weight: 500; }
|
|
76
|
+
.srv { display: block; width: 100%; text-align: left; border: 0; background: none; box-shadow: none; padding: 6px 10px; border-radius: var(--r-sm); }
|
|
77
|
+
.srv:hover { background: var(--hover); }
|
|
78
|
+
.srv.on { background: var(--panel); box-shadow: 0 0 0 1px var(--line), var(--shadow); }
|
|
79
|
+
.srv.on .n { color: var(--text); }
|
|
80
|
+
.srv.on .t { color: var(--dim); }
|
|
81
|
+
.srv .n { font-weight: 550; display: flex; align-items: center; gap: 6px; }
|
|
82
|
+
.srv .t { color: var(--faint); font-size: var(--fs-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
78
83
|
.nav-none { color: var(--faint); font-size: var(--fs-sm); padding: 6px 10px; }
|
|
79
84
|
.srv .t .live { color: var(--ok); }
|
|
80
85
|
/* The open server's sections, under it in the list. */
|
|
81
|
-
.sections { display: grid; gap: 1px; margin:
|
|
82
|
-
.sections button { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; border: 0; background: none; padding:
|
|
83
|
-
.sections button:hover { background: var(--
|
|
84
|
-
.sections button.on { background: var(--accent-bg); color: var(--accent); font-weight:
|
|
86
|
+
.sections { display: grid; gap: 1px; margin: 4px 0 10px 12px; padding-left: 8px; border-left: 1px solid var(--line-strong); }
|
|
87
|
+
.sections button { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; border: 0; background: none; box-shadow: none; padding: 5px 10px; color: var(--dim); font-weight: 450; }
|
|
88
|
+
.sections button:hover { background: var(--hover); color: var(--text); }
|
|
89
|
+
.sections button.on { background: var(--accent-bg); color: var(--accent); font-weight: 550; }
|
|
85
90
|
.sections .badge { margin-left: auto; }
|
|
86
|
-
.head h1 .crumb { font-weight: 400; color: var(--
|
|
91
|
+
.head h1 .crumb { font-weight: 400; color: var(--faint); }
|
|
87
92
|
.tunnel-url { font-family: var(--mono); font-size: var(--fs-sm); }
|
|
88
93
|
.tunnel-lan::before { content: "· LAN "; color: var(--faint); font-size: var(--fs-sm); }
|
|
89
94
|
.star { color: var(--warn); font-size: var(--fs-xs); }
|
|
90
|
-
main { overflow: auto; padding:
|
|
95
|
+
main { overflow: auto; padding: 28px 32px 48px; min-width: 0; container-type: inline-size; }
|
|
91
96
|
.show-sm { display: none !important; }
|
|
92
97
|
/* By main's own width, so an open job panel squeezes the tables like a small screen does. */
|
|
93
98
|
@container (max-width: 720px) {
|
|
94
|
-
td, th { padding: 8px; }
|
|
99
|
+
td, th { padding: 9px 8px; }
|
|
95
100
|
.hide-sm { display: none; }
|
|
96
101
|
.show-sm { display: flex !important; }
|
|
97
102
|
}
|
|
98
|
-
.empty { color: var(--dim); text-align: center; padding:
|
|
99
|
-
.empty h2 { color: var(--text); font-size: 18px; margin: 0 0 6px; }
|
|
100
|
-
.head { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 12px 20px; margin-bottom:
|
|
103
|
+
.empty { color: var(--dim); text-align: center; padding: 72px 20px; }
|
|
104
|
+
.empty h2 { color: var(--text); font-size: 18px; font-weight: 600; letter-spacing: -.01em; margin: 0 0 6px; }
|
|
105
|
+
.head { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 12px 20px; margin-bottom: 22px; }
|
|
101
106
|
.head > div:first-child { flex: 1 1 320px; min-width: 0; }
|
|
102
|
-
.head h1 { margin: 0; font-size:
|
|
103
|
-
.head .sub { color: var(--dim); font-size: var(--fs); margin-top:
|
|
107
|
+
.head h1 { margin: 0; font-size: 24px; font-weight: 650; letter-spacing: -.02em; line-height: 1.25; }
|
|
108
|
+
.head .sub { color: var(--dim); font-size: var(--fs); margin-top: 4px; }
|
|
104
109
|
.actions { display: flex; flex-wrap: wrap; gap: 6px; margin-left: auto; }
|
|
105
110
|
/* Phones only: elsewhere the sections sit under the server in the list. */
|
|
106
|
-
.tabs { display: none; gap:
|
|
111
|
+
.tabs { display: none; gap: 4px; padding: 4px; background: var(--hover); border-radius: var(--r); margin-bottom: 18px; overflow-x: auto; scrollbar-width: none; }
|
|
107
112
|
.tabs::-webkit-scrollbar { display: none; }
|
|
108
|
-
.tabs button { border: 0; border-
|
|
109
|
-
.tabs button:hover { color: var(--text); }
|
|
110
|
-
.tabs button.on { color: var(--text);
|
|
111
|
-
.card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg); box-shadow: var(--shadow); }
|
|
112
|
-
.card + .card { margin-top:
|
|
113
|
-
.card > h2 { margin: 0; padding:
|
|
113
|
+
.tabs button { border: 0; border-radius: var(--r-sm); background: none; box-shadow: none; padding: 6px 12px; color: var(--dim); flex: none; }
|
|
114
|
+
.tabs button:hover { color: var(--text); background: none; }
|
|
115
|
+
.tabs button.on { color: var(--text); background: var(--panel); box-shadow: var(--shadow), 0 0 0 1px var(--line); }
|
|
116
|
+
.card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg); box-shadow: var(--shadow); overflow: clip; }
|
|
117
|
+
.card + .card { margin-top: 20px; }
|
|
118
|
+
.card > h2 { margin: 0; padding: 14px 18px; font-size: 14px; font-weight: 600; border-bottom: 1px solid var(--line); display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; }
|
|
114
119
|
.card > h2 .actions { font-weight: 400; }
|
|
115
120
|
.card > h2 .sub { font-weight: 400; color: var(--dim); }
|
|
116
|
-
.card > .lede { padding: 10px
|
|
121
|
+
.card > .lede { padding: 10px 18px; color: var(--dim); font-size: var(--fs); border-bottom: 1px solid var(--line); }
|
|
117
122
|
|
|
118
123
|
/* "⋯" menus: one floating list at a time, placed by openMenu(). */
|
|
119
|
-
button.more { padding-inline:
|
|
120
|
-
.menu { position: fixed; z-index: 30; min-width:
|
|
121
|
-
.menu button { border: 0; background: none; text-align: left; padding: 6px 10px; border-radius: var(--r-sm); display: flex; justify-content: space-between; gap: 16px; }
|
|
122
|
-
.menu button:hover { background: var(--
|
|
123
|
-
.menu button:focus-visible { background: var(--accent-bg);
|
|
124
|
+
button.more { padding-inline: 8px; letter-spacing: .05em; }
|
|
125
|
+
.menu { position: fixed; z-index: 30; min-width: 190px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--r); box-shadow: var(--shadow-lg); padding: 5px; display: grid; animation: pop .12s ease-out; }
|
|
126
|
+
.menu button { border: 0; background: none; box-shadow: none; font-weight: 450; text-align: left; padding: 6px 10px; border-radius: var(--r-sm); display: flex; justify-content: space-between; gap: 16px; }
|
|
127
|
+
.menu button:hover { background: var(--hover); }
|
|
128
|
+
.menu button:focus-visible { background: var(--accent-bg); box-shadow: none; }
|
|
124
129
|
.menu button.danger:hover { background: var(--bad-bg); }
|
|
125
130
|
.menu button .k { color: var(--faint); font-family: var(--mono); font-size: var(--fs-xs); }
|
|
126
131
|
.menu hr { border: 0; border-top: 1px solid var(--line); margin: 4px 2px; }
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
.home
|
|
130
|
-
.home
|
|
131
|
-
.
|
|
132
|
-
.
|
|
133
|
-
.tile
|
|
132
|
+
@keyframes pop { from { opacity: 0; transform: translateY(-3px) scale(.98); } }
|
|
133
|
+
|
|
134
|
+
.home { max-width: 880px; margin: 0 auto; padding-top: 24px; }
|
|
135
|
+
.home h1 { margin: 0 0 6px; font-size: 28px; font-weight: 650; letter-spacing: -.025em; }
|
|
136
|
+
.home > p { color: var(--dim); margin: 0 0 24px; }
|
|
137
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 12px; }
|
|
138
|
+
.tile { display: block; text-align: left; padding: 16px 18px; border-radius: var(--r-lg); background: var(--panel); border: 1px solid var(--line); box-shadow: var(--shadow); width: 100%; transition: border-color .12s, box-shadow .15s, transform .15s; }
|
|
139
|
+
.tile:hover { background: var(--panel); border-color: var(--line-strong); box-shadow: var(--shadow-lg); transform: translateY(-1px); }
|
|
134
140
|
.tile .n { font-weight: 600; font-size: 15px; display: flex; align-items: center; gap: 6px; }
|
|
135
|
-
.tile .t { color: var(--
|
|
136
|
-
.tile.add { border
|
|
137
|
-
.
|
|
138
|
-
.cli
|
|
141
|
+
.tile .t { color: var(--faint); font-size: var(--fs-sm); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
142
|
+
.tile.add { border: 1.5px dashed var(--line-strong); background: none; box-shadow: none; color: var(--dim); display: grid; place-items: center; min-height: 68px; }
|
|
143
|
+
.tile.add:hover { color: var(--accent); border-color: var(--accent); background: var(--accent-bg); transform: none; box-shadow: none; }
|
|
144
|
+
.cli { margin-top: 32px; padding: 16px 18px; border-radius: var(--r-lg); border: 1px solid var(--line); background: var(--hover); font-size: var(--fs); color: var(--dim); }
|
|
145
|
+
.cli pre { margin: 10px 0 0; font: 12.5px/1.7 var(--mono); color: var(--text); white-space: pre-wrap; }
|
|
139
146
|
.cli pre span { color: var(--faint); }
|
|
140
147
|
|
|
141
|
-
.drow { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 8px 16px; padding:
|
|
148
|
+
.drow { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 8px 16px; padding: 12px 0; border-bottom: 1px solid var(--line); }
|
|
142
149
|
.drow:first-child { padding-top: 0; }
|
|
150
|
+
.drow:last-child { border-bottom: 0; padding-bottom: 0; }
|
|
143
151
|
.drow > div:first-child { flex: 1 1 260px; min-width: 0; }
|
|
144
152
|
.drow .actions { flex: none; }
|
|
145
153
|
.site-name { display: flex; align-items: center; gap: 2px; white-space: nowrap; }
|
|
146
154
|
td .chips.show-sm { margin-top: 4px; }
|
|
147
|
-
.card .body { padding:
|
|
155
|
+
.card .body { padding: 16px 18px; }
|
|
148
156
|
table { border-collapse: collapse; width: 100%; }
|
|
149
|
-
th { text-align: left; font-weight:
|
|
150
|
-
td { padding:
|
|
157
|
+
th { text-align: left; font-weight: 500; font-size: var(--fs-sm); color: var(--faint); padding: 9px 14px; border-bottom: 1px solid var(--line); background: color-mix(in srgb, var(--hover) 55%, transparent); }
|
|
158
|
+
td { padding: 11px 14px; border-bottom: 1px solid var(--line); vertical-align: middle; }
|
|
151
159
|
tr:last-child td { border-bottom: 0; }
|
|
152
160
|
tr.click { cursor: pointer; }
|
|
153
|
-
tr.click:hover td { background: var(--
|
|
161
|
+
tr.click:hover td { background: var(--hover); }
|
|
154
162
|
td.acts { text-align: right; white-space: nowrap; }
|
|
155
163
|
.nowrap { white-space: nowrap; }
|
|
156
164
|
td.acts button { margin-left: 4px; }
|
|
157
165
|
.scroll { overflow-x: auto; }
|
|
158
|
-
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--faint); margin-right:
|
|
159
|
-
.dot.up { background: var(--ok); } .dot.down { background: var(--bad); } .dot.warn { background: var(--warn); }
|
|
160
|
-
.badge { display: inline-block; font-size: var(--fs-xs); padding:
|
|
166
|
+
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--faint); margin-right: 7px; vertical-align: 1px; }
|
|
167
|
+
.dot.up { background: var(--ok); box-shadow: 0 0 0 3px var(--ok-bg); } .dot.down { background: var(--bad); box-shadow: 0 0 0 3px var(--bad-bg); } .dot.warn { background: var(--warn); box-shadow: 0 0 0 3px var(--warn-bg); }
|
|
168
|
+
.badge { display: inline-block; font-size: var(--fs-xs); font-weight: 500; line-height: 1.6; padding: 0 8px; border-radius: 99px; background: var(--hover); color: var(--dim); border: 1px solid var(--line); }
|
|
161
169
|
.badge.ok { background: var(--ok-bg); color: var(--ok); border-color: transparent; }
|
|
162
170
|
.badge.warn { background: var(--warn-bg); color: var(--warn); border-color: transparent; }
|
|
163
171
|
.badge.bad { background: var(--bad-bg); color: var(--bad); border-color: transparent; }
|
|
164
172
|
.badge.accent { background: var(--accent-bg); color: var(--accent); border-color: transparent; }
|
|
165
173
|
.dim { color: var(--dim); } .faint { color: var(--faint); }
|
|
166
|
-
.note { padding:
|
|
174
|
+
.note { padding: 11px 14px; border-radius: var(--r); background: var(--warn-bg); color: var(--warn); margin-bottom: 16px; border: 1px solid color-mix(in srgb, currentColor 18%, transparent); }
|
|
167
175
|
.note.bad { background: var(--bad-bg); color: var(--bad); }
|
|
168
176
|
.note.info { background: var(--accent-bg); color: var(--accent); }
|
|
169
|
-
.kv { display: grid; grid-template-columns: max-content 1fr; gap:
|
|
170
|
-
.kv dt { color: var(--
|
|
171
|
-
pre.block { background: var(--
|
|
172
|
-
.detail td { background: var(--
|
|
177
|
+
.kv { display: grid; grid-template-columns: max-content 1fr; gap: 6px 18px; font-size: var(--fs); margin: 0; }
|
|
178
|
+
.kv dt { color: var(--faint); } .kv dd { margin: 0; }
|
|
179
|
+
pre.block { background: var(--hover); border: 1px solid var(--line); padding: 10px 12px; border-radius: var(--r-sm); overflow-x: auto; font-size: var(--fs-sm); margin: 6px 0 0; }
|
|
180
|
+
.detail td { background: color-mix(in srgb, var(--hover) 60%, transparent); }
|
|
173
181
|
.releases, .services { margin: 0; padding: 0; list-style: none; }
|
|
174
|
-
.services li { display: flex; align-items: center; gap: 8px; padding:
|
|
175
|
-
.releases li { display: flex; align-items: center; gap: 10px; padding:
|
|
176
|
-
.run { padding:
|
|
182
|
+
.services li { display: flex; align-items: center; gap: 8px; padding: 3px 0; }
|
|
183
|
+
.releases li { display: flex; align-items: center; gap: 10px; padding: 5px 0; }
|
|
184
|
+
.run { padding: 14px 18px; border-bottom: 1px solid var(--line); }
|
|
177
185
|
.run:last-child { border-bottom: 0; }
|
|
178
186
|
.run .top { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
179
187
|
.run .cmd { font-family: var(--mono); font-weight: 600; font-size: var(--fs); }
|
|
@@ -184,49 +192,50 @@ pre.block { background: var(--bg); border: 1px solid var(--line); padding: 10px
|
|
|
184
192
|
.steps li.undone .what { text-decoration: line-through; }
|
|
185
193
|
.steps li button { opacity: 0; }
|
|
186
194
|
.steps li:hover button, .steps li:focus-within button { opacity: 1; }
|
|
187
|
-
.form-row { display: flex; gap:
|
|
195
|
+
.form-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: end; }
|
|
188
196
|
.form-row label { flex: 1 1 140px; }
|
|
189
|
-
label { display: block; font-size: var(--fs-sm); color: var(--dim); }
|
|
190
|
-
label > input, label > select, label > textarea { margin-top:
|
|
191
|
-
.check { display: flex; gap: 8px; align-items: flex-start; color: var(--text); font-size: var(--fs); }
|
|
192
|
-
.check input { width: auto; margin-top: 3px; }
|
|
193
|
-
.hint { font-size: var(--fs-sm); color: var(--faint); margin-top:
|
|
194
|
-
.spin { display: inline-block; width: 12px; height: 12px; border: 2px solid var(--line); border-top-color: var(--accent); border-radius: 50%; animation: spin .8s linear infinite; vertical-align: -2px; }
|
|
197
|
+
label { display: block; font-size: var(--fs-sm); font-weight: 500; color: var(--dim); }
|
|
198
|
+
label > input, label > select, label > textarea { margin-top: 5px; font-weight: 400; }
|
|
199
|
+
.check { display: flex; gap: 8px; align-items: flex-start; color: var(--text); font-size: var(--fs); font-weight: 400; }
|
|
200
|
+
.check input { width: auto; margin-top: 3px; accent-color: var(--accent-solid); }
|
|
201
|
+
.hint { font-size: var(--fs-sm); font-weight: 400; color: var(--faint); margin-top: 4px; }
|
|
202
|
+
.spin { display: inline-block; width: 12px; height: 12px; border: 2px solid var(--line-strong); border-top-color: var(--accent); border-radius: 50%; animation: spin .8s linear infinite; vertical-align: -2px; }
|
|
195
203
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
196
|
-
@media (prefers-reduced-motion: reduce) { .spin { animation-duration: 3s; } }
|
|
197
|
-
.loading { color: var(--dim); padding:
|
|
204
|
+
@media (prefers-reduced-motion: reduce) { .spin { animation-duration: 3s; } .menu, dialog[open] { animation: none; } .tile:hover { transform: none; } }
|
|
205
|
+
.loading { color: var(--dim); padding: 40px 0; text-align: center; }
|
|
198
206
|
|
|
199
207
|
[hidden] { display: none !important; }
|
|
200
208
|
aside.job { border-left: 1px solid var(--line); background: var(--panel); display: flex; flex-direction: column; min-width: 0; }
|
|
201
|
-
.job-head { padding: 10px
|
|
202
|
-
.job-head .title { font-family: var(--mono); font-size: var(--fs-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
|
|
203
|
-
.job-out { flex: 1; overflow: auto; background: var(--term); color: var(--term-text); font: 12.5px/1.
|
|
204
|
-
.job-out .c-cyan { color: #67d3e8; } .job-out .c-green { color: #6ee7a0; } .job-out .c-yellow { color: #f5c76a; } .job-out .c-red { color: #ff8a8a; } .job-out .c-dim { color: #
|
|
205
|
-
.job-foot { padding: 10px
|
|
209
|
+
.job-head { padding: 10px 14px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 8px; min-height: 48px; }
|
|
210
|
+
.job-head .title { font-family: var(--mono); font-size: var(--fs-sm); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
|
|
211
|
+
.job-out { flex: 1; overflow: auto; background: var(--term); color: var(--term-text); font: 12.5px/1.6 var(--mono); padding: 12px 14px; white-space: pre-wrap; word-break: break-word; }
|
|
212
|
+
.job-out .c-cyan { color: #67d3e8; } .job-out .c-green { color: #6ee7a0; } .job-out .c-yellow { color: #f5c76a; } .job-out .c-red { color: #ff8a8a; } .job-out .c-dim { color: #7b7b86; }
|
|
213
|
+
.job-foot { padding: 10px 14px; border-top: 1px solid var(--line); display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
|
|
206
214
|
.job-foot .status { margin-right: auto; font-size: var(--fs); }
|
|
207
|
-
.joblist { border-top: 1px solid var(--line); max-height: 28%; overflow: auto; }
|
|
208
|
-
.joblist button { display: flex; width: 100%; gap: 8px; border: 0; border-radius:
|
|
215
|
+
.joblist { border-top: 1px solid var(--line); max-height: 28%; overflow: auto; padding: 4px; }
|
|
216
|
+
.joblist button { display: flex; width: 100%; gap: 8px; border: 0; border-radius: var(--r-sm); box-shadow: none; text-align: left; background: none; padding: 6px 10px; font-family: var(--mono); font-size: var(--fs-sm); font-weight: 400; color: var(--dim); }
|
|
209
217
|
.joblist button span:last-child { overflow: hidden; text-overflow: ellipsis; }
|
|
210
|
-
.joblist button.on { background: var(--accent-bg); color: var(--text);
|
|
218
|
+
.joblist button.on { background: var(--accent-bg); color: var(--text); }
|
|
211
219
|
.joblist .ok { color: var(--ok); } .joblist .bad { color: var(--bad); }
|
|
212
220
|
|
|
213
|
-
dialog { border: 1px solid var(--line); border-radius: var(--r-lg); padding: 0; background: var(--panel); color: var(--text); box-shadow: var(--shadow); width: min(520px, calc(100vw - 32px)); }
|
|
214
|
-
dialog
|
|
215
|
-
dialog
|
|
216
|
-
dialog
|
|
221
|
+
dialog { border: 1px solid var(--line); border-radius: var(--r-lg); padding: 0; background: var(--panel); color: var(--text); box-shadow: var(--shadow-lg); width: min(520px, calc(100vw - 32px)); }
|
|
222
|
+
dialog[open] { animation: pop .15s ease-out; }
|
|
223
|
+
dialog::backdrop { background: rgb(9 9 11 / .4); backdrop-filter: blur(2px); }
|
|
224
|
+
dialog form { padding: 22px 24px; display: grid; gap: 14px; }
|
|
225
|
+
dialog h2 { margin: 0; font-size: 17px; font-weight: 600; letter-spacing: -.01em; }
|
|
217
226
|
dialog .text { color: var(--dim); white-space: pre-wrap; font-size: var(--fs); }
|
|
218
|
-
dialog .btns { display: flex; justify-content: flex-end; gap: 8px; margin-top:
|
|
219
|
-
fieldset { border: 1px solid var(--line); border-radius: var(--r); padding:
|
|
227
|
+
dialog .btns { display: flex; justify-content: flex-end; gap: 8px; margin-top: 6px; }
|
|
228
|
+
fieldset { border: 1px solid var(--line); border-radius: var(--r); padding: 12px 14px; margin: 0; display: grid; gap: 10px; }
|
|
220
229
|
legend { font-size: var(--fs-sm); font-weight: 600; padding: 0 4px; }
|
|
221
|
-
.toast { position: fixed; inset: auto auto
|
|
222
|
-
.toast button { margin-left: 10px; background: none; border-color: color-mix(in srgb, var(--bg) 40%, transparent); color: inherit; }
|
|
230
|
+
.toast { position: fixed; inset: auto auto 20px 50%; transform: translateX(-50%); margin: 0; border: 0; background: var(--text); color: var(--bg); padding: 10px 16px; border-radius: var(--r); box-shadow: var(--shadow-lg); font-size: var(--fs); font-weight: 500; max-width: min(560px, calc(100vw - 32px)); }
|
|
231
|
+
.toast button { margin-left: 10px; background: none; box-shadow: none; border-color: color-mix(in srgb, var(--bg) 40%, transparent); color: inherit; }
|
|
223
232
|
.sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
|
224
|
-
.row-toggle { border: 0; background: none; padding: 2px 4px; margin-left: -4px; display: inline-flex; align-items: center; gap: 4px; color: inherit; font-weight: 600; }
|
|
225
|
-
.row-toggle:hover { background: none; text-decoration: underline; }
|
|
233
|
+
.row-toggle { border: 0; background: none; box-shadow: none; padding: 2px 4px; margin-left: -4px; display: inline-flex; align-items: center; gap: 4px; color: inherit; font-weight: 600; }
|
|
234
|
+
.row-toggle:hover { background: none; text-decoration: underline; text-underline-offset: 2px; }
|
|
226
235
|
.jobs-btn .spin { width: 10px; height: 10px; margin-right: 4px; }
|
|
227
236
|
.dlg-err { margin: 0; }
|
|
228
237
|
.server-pick { display: none; }
|
|
229
|
-
.facts { display: flex; flex-wrap: wrap; gap: 4px
|
|
238
|
+
.facts { display: flex; flex-wrap: wrap; gap: 4px 16px; color: var(--faint); font-size: var(--fs-sm); margin-top: 6px; }
|
|
230
239
|
.facts b { color: var(--text); font-weight: 500; }
|
|
231
240
|
.chips { display: flex; flex-direction: column; gap: 3px; align-items: flex-start; }
|
|
232
241
|
.chip { display: inline-flex; align-items: center; gap: 2px; max-width: 100%; }
|
|
@@ -236,14 +245,14 @@ legend { font-size: var(--fs-sm); font-weight: 600; padding: 0 4px; }
|
|
|
236
245
|
.lock { font-size: var(--fs-xs); margin-left: 4px; }
|
|
237
246
|
dialog.wide { width: min(680px, calc(100vw - 32px)); }
|
|
238
247
|
.list { display: grid; border: 1px solid var(--line); border-radius: var(--r); max-height: 260px; overflow: auto; }
|
|
239
|
-
.list .row { display: flex; gap: 10px; align-items: baseline; border: 0; border-radius: 0; border-bottom: 1px solid var(--line); text-align: left; background: none; padding:
|
|
248
|
+
.list .row { display: flex; gap: 10px; align-items: baseline; border: 0; border-radius: 0; box-shadow: none; font-weight: 400; border-bottom: 1px solid var(--line); text-align: left; background: none; padding: 7px 12px; min-width: 0; }
|
|
240
249
|
.list .row:last-child { border-bottom: 0; }
|
|
241
|
-
.list .row:hover { background: var(--
|
|
250
|
+
.list .row:hover { background: var(--hover); }
|
|
242
251
|
.list .row .mono { margin-left: auto; overflow: hidden; text-overflow: ellipsis; }
|
|
243
252
|
.crumbs { display: flex; flex-wrap: wrap; gap: 2px; align-items: center; font-size: var(--fs-sm); }
|
|
244
|
-
.crumbs button { padding: 1px
|
|
245
|
-
.detected { font-size: var(--fs); padding:
|
|
246
|
-
.drop { border: 1.5px dashed var(--line); border-radius: var(--r); padding:
|
|
253
|
+
.crumbs button { padding: 1px 6px; }
|
|
254
|
+
.detected { font-size: var(--fs); padding: 10px 12px; border-radius: var(--r); background: var(--hover); }
|
|
255
|
+
.drop { border: 1.5px dashed var(--line-strong); border-radius: var(--r); padding: 14px; text-align: center; color: var(--dim); font-size: var(--fs); }
|
|
247
256
|
.drop label { display: inline; color: var(--accent); cursor: pointer; font-size: var(--fs); }
|
|
248
257
|
body.dragging::after {
|
|
249
258
|
content: "Drop a folder or .zip to deploy it"; position: fixed; inset: 12px; z-index: 20; display: grid; place-items: center;
|
|
@@ -252,19 +261,22 @@ body.dragging::after {
|
|
|
252
261
|
|
|
253
262
|
@media (max-width: 1100px) {
|
|
254
263
|
.app.with-job { grid-template-columns: 220px minmax(0, 1fr); }
|
|
255
|
-
aside.job { position: fixed; inset:
|
|
264
|
+
aside.job { position: fixed; inset: 56px 0 0 auto; width: min(560px, 100vw); z-index: 5; box-shadow: var(--shadow-lg); }
|
|
256
265
|
}
|
|
257
266
|
@media (max-width: 760px) {
|
|
258
|
-
.app, .app.with-job { grid-template-columns: minmax(0, 1fr); grid-template-rows:
|
|
259
|
-
header { padding: 0
|
|
267
|
+
.app, .app.with-job { grid-template-columns: minmax(0, 1fr); grid-template-rows: 56px auto minmax(0, 1fr); }
|
|
268
|
+
header { padding: 0 14px; gap: 8px; }
|
|
260
269
|
.brand small { display: none; }
|
|
261
|
-
.project button { padding: 5px
|
|
270
|
+
.project button { padding: 5px 10px; font-size: var(--fs); white-space: nowrap; }
|
|
262
271
|
nav.servers { display: none; }
|
|
263
|
-
.server-pick { display: block; padding:
|
|
264
|
-
.tabs { display: flex; position: sticky; top: -14px; z-index: 2;
|
|
265
|
-
main { padding:
|
|
272
|
+
.server-pick { display: block; padding: 12px 16px 0; }
|
|
273
|
+
.tabs { display: flex; position: sticky; top: -14px; z-index: 2; }
|
|
274
|
+
main { padding: 16px 16px 40px; }
|
|
275
|
+
.home h1 { font-size: 24px; }
|
|
266
276
|
.project .path { display: none; }
|
|
267
|
-
aside.job { inset:
|
|
277
|
+
aside.job { inset: 56px 0 0 0; width: 100vw; }
|
|
278
|
+
td.acts { white-space: normal; }
|
|
279
|
+
td.acts button { margin: 2px 0 2px 4px; }
|
|
268
280
|
}
|
|
269
281
|
</style>
|
|
270
282
|
</head>
|
|
@@ -506,7 +518,10 @@ async function refreshState() {
|
|
|
506
518
|
async function run(argv, o = {}) {
|
|
507
519
|
let info;
|
|
508
520
|
try {
|
|
509
|
-
|
|
521
|
+
// A retry runs the job's own argv on the server: the page only has it with passwords masked.
|
|
522
|
+
info = o.retry
|
|
523
|
+
? await http(`/api/jobs/${encodeURIComponent(o.retry)}/force`, {})
|
|
524
|
+
: await http("/api/jobs", { argv, server: o.server ?? S.sel ?? undefined, password: o.password, cwd: o.cwd });
|
|
510
525
|
} catch (e) {
|
|
511
526
|
return toast(e.message);
|
|
512
527
|
}
|
|
@@ -522,7 +537,8 @@ function stream(j) {
|
|
|
522
537
|
const es = new EventSource(`/api/jobs/${j.info.id}/events?from=${j.lines.length}`);
|
|
523
538
|
j.es = es;
|
|
524
539
|
es.onmessage = (e) => {
|
|
525
|
-
const { from, lines } = JSON.parse(e.data);
|
|
540
|
+
const { from, lines, report } = JSON.parse(e.data);
|
|
541
|
+
if (report) j.report = report;
|
|
526
542
|
j.lines.length = Math.min(j.lines.length, from);
|
|
527
543
|
j.lines.push(...lines);
|
|
528
544
|
if (S.job === j.info.id) appendOutput(j);
|
|
@@ -532,6 +548,7 @@ function stream(j) {
|
|
|
532
548
|
es.addEventListener("done", (e) => {
|
|
533
549
|
es.close();
|
|
534
550
|
j.info = JSON.parse(e.data);
|
|
551
|
+
j.report = j.info.report;
|
|
535
552
|
finished(j);
|
|
536
553
|
});
|
|
537
554
|
// Dropped connection while it still runs: pick up where it left off.
|
|
@@ -546,12 +563,12 @@ function finished(j) {
|
|
|
546
563
|
const failed = info.code !== 0 && info.code !== null;
|
|
547
564
|
j.actions = (j.after && j.after(j)) || [];
|
|
548
565
|
// Safety checks refuse what would lock out ssh or break running services; --force overrides after reading why.
|
|
549
|
-
|
|
550
|
-
|
|
566
|
+
const why = rep(j).force;
|
|
567
|
+
if (failed && !info.argv.includes("--force") && why) {
|
|
551
568
|
j.actions.push({
|
|
552
569
|
label: "Do it anyway (--force)…",
|
|
553
570
|
cls: "danger",
|
|
554
|
-
run: () => confirmDlg({ title: "Override the safety check?", text: why, ok: "Run with --force", danger: true, onOk: () => run([...info.argv, "--force"], { server: info.server }) }),
|
|
571
|
+
run: () => confirmDlg({ title: "Override the safety check?", text: why, ok: "Run with --force", danger: true, onOk: () => run([...info.argv, "--force"], { server: info.server, retry: info.id }) }),
|
|
555
572
|
});
|
|
556
573
|
}
|
|
557
574
|
if (info.undoable && info.server && info.code !== null) {
|
|
@@ -583,9 +600,11 @@ const runningJobs = () => [...S.jobs.values()].filter((j) => j.info.code === und
|
|
|
583
600
|
const isTunnel = (info) => info.argv[0] === "tunnel" || (info.argv[0] === "traefik" && info.argv[1] === "dashboard");
|
|
584
601
|
/** Tunnels the UI opened to server n that are still open. */
|
|
585
602
|
const openTunnels = (n) => runningJobs().filter((j) => j.info.server === n && isTunnel(j.info));
|
|
586
|
-
|
|
603
|
+
/** What the job reported through the CLI's report(): never parsed from its text output. */
|
|
604
|
+
const rep = (j) => j.report ?? j.info.report ?? {};
|
|
605
|
+
const tunnelUrl = (j) => rep(j).url;
|
|
587
606
|
/** With --lan: the addresses other devices on the network open it at. */
|
|
588
|
-
const lanUrls = (j) => j
|
|
607
|
+
const lanUrls = (j) => rep(j).lan ?? [];
|
|
589
608
|
const closeTunnel = (j) => http(`/api/jobs/${j.info.id}/stop`, {}).catch((e) => toast(e.message));
|
|
590
609
|
/** argv plus --port, --local and --lan, picked in a dialog. ports: the ones the container listens on, if known. */
|
|
591
610
|
function tunnelOptions(n, argv, ports = []) {
|
|
@@ -615,7 +634,13 @@ function tunnelOptions(n, argv, ports = []) {
|
|
|
615
634
|
function tunnelBtn(n, argv, label = "Open tunnel", ports = []) {
|
|
616
635
|
// Opened with options, the job's argv goes on after argv.
|
|
617
636
|
const j = openTunnels(n).find((t) => t.info.argv.slice(0, argv.length).join(" ") === argv.join(" "));
|
|
618
|
-
if (!j)
|
|
637
|
+
if (!j)
|
|
638
|
+
return `${btn(label, () => run(argv, { server: n }), "sm", `lf ${argv.join(" ")}: until you close it`)}${btn(
|
|
639
|
+
"On network",
|
|
640
|
+
() => run([...argv, "--lan"], { server: n }),
|
|
641
|
+
"sm",
|
|
642
|
+
"Open it for every device on your network too (a phone, a colleague): anyone there reaches it at this machine's address",
|
|
643
|
+
)}${btn("…", () => tunnelOptions(n, argv, ports), "sm", "Container port, local port, network")}`;
|
|
619
644
|
const url = tunnelUrl(j);
|
|
620
645
|
// A database's address is for a client, not the browser.
|
|
621
646
|
const link = (u) => u.startsWith("http://") ? `<a class="tunnel-url" href="${esc(u)}" target="_blank" rel="noopener">${esc(u.replace("http://", ""))}</a>` : `<code class="tunnel-url" title="Connect a client to it">${esc(u)}</code>`;
|
|
@@ -785,9 +810,9 @@ function dialog({ title, text = "", fields = [], ok = "OK", danger = false, onOk
|
|
|
785
810
|
}
|
|
786
811
|
const invalid = (field, error) => ({ field, error });
|
|
787
812
|
/** What is wrong with a login (lf newLogin's rules), as [field, message]. */
|
|
788
|
-
const loginError = (user, password) =>
|
|
789
|
-
|
|
790
|
-
: password.length <
|
|
813
|
+
const loginError = (user, password, { loginName, minPassword } = S.state.rules) =>
|
|
814
|
+
!new RegExp(loginName).test(user) ? ["user", "Only letters, digits and . _ @ -"]
|
|
815
|
+
: password.length < minPassword ? ["password", `The password needs at least ${minPassword} characters`]
|
|
791
816
|
: null;
|
|
792
817
|
/**
|
|
793
818
|
* Passwords here are for servers and sites, not for this page: a masked text field, so browsers
|
|
@@ -816,10 +841,10 @@ function setupFlow(args, base = args) {
|
|
|
816
841
|
server: args[0],
|
|
817
842
|
after: (j) => {
|
|
818
843
|
if (j.info.code !== 0) return;
|
|
819
|
-
if (j
|
|
844
|
+
if (rep(j).needsKey) {
|
|
820
845
|
return [{ label: "Enter the password to install a key", run: () => passwordFor(args, base) }];
|
|
821
846
|
}
|
|
822
|
-
const todo = j.
|
|
847
|
+
const todo = (rep(j).checks ?? []).filter((ch) => ch.status === "todo").length;
|
|
823
848
|
return [
|
|
824
849
|
{ label: todo ? `Apply ${plural(todo, "change")}` : "Finish setup (nothing to install)", run: () => run(["setup", ...args, "-y"], { server: args[0] }) },
|
|
825
850
|
{ label: "Change options", run: () => setupWizard(base) },
|
|
@@ -932,14 +957,13 @@ function addServer() {
|
|
|
932
957
|
}
|
|
933
958
|
|
|
934
959
|
const defaultServer = () => S.sel ?? S.state.servers.find((s) => s.isDefault)?.name;
|
|
935
|
-
const sslip = (name, ip) => `${name}.${ip.replace(/\./g, "-")}.sslip.io`;
|
|
936
960
|
const mainDomain = (n) => S.state.servers.find((s) => s.name === n)?.saved?.domains?.[0];
|
|
937
961
|
/** What `lf deploy` picks without a domain: <site>.<main domain>, else the free sslip.io address. */
|
|
938
962
|
function autoDomain(n, name) {
|
|
939
963
|
const main = mainDomain(n);
|
|
940
964
|
if (main) return `${name}.${main}`;
|
|
941
|
-
const
|
|
942
|
-
return
|
|
965
|
+
const suffix = S.facts[n]?.data?.sslipSuffix;
|
|
966
|
+
return suffix ? `${name}.${suffix}` : "";
|
|
943
967
|
}
|
|
944
968
|
const slugify = (s) => s.toLowerCase().replace(/\.(zip|tgz|tar(\.gz)?)$/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "site";
|
|
945
969
|
const tilde = (p) => p.replace(/^\/(Users|home)\/[^/]+/, "~");
|
|
@@ -989,7 +1013,7 @@ function deployDialog(det, o = {}) {
|
|
|
989
1013
|
{ name: "spa", label: "Single-page app (unknown paths serve index.html)", type: "check", value: !!p?.spa },
|
|
990
1014
|
]
|
|
991
1015
|
: []),
|
|
992
|
-
{ name: "authPassword", label:
|
|
1016
|
+
{ name: "authPassword", label: `Login password (optional, at least ${S.state.rules.minPassword} characters)`, type: "password", hint: "Visitors log in with the user below and this password (HTTP basic auth). Empty: no change; set or remove it later with Password… on the site." },
|
|
993
1017
|
{ name: "authUser", label: "Login user", value: "admin", placeholder: "admin" },
|
|
994
1018
|
],
|
|
995
1019
|
ok: "Deploy",
|
|
@@ -1021,7 +1045,7 @@ function deployDialog(det, o = {}) {
|
|
|
1021
1045
|
/** After a deploy: offer the address it printed. */
|
|
1022
1046
|
function openAfter(j) {
|
|
1023
1047
|
if (j.info.code !== 0) return;
|
|
1024
|
-
const url = j
|
|
1048
|
+
const url = rep(j).url;
|
|
1025
1049
|
if (url) return [{ label: `Open ${url.replace("https://", "")}`, run: () => window.open(url, "_blank", "noopener") }];
|
|
1026
1050
|
}
|
|
1027
1051
|
|
|
@@ -1180,7 +1204,7 @@ function authDialog(n, m) {
|
|
|
1180
1204
|
: "Visitors have to log in (HTTP basic auth) before they see the site.",
|
|
1181
1205
|
fields: [
|
|
1182
1206
|
{ name: "user", label: "User", value: users[0] ?? "admin" },
|
|
1183
|
-
{ name: "password", label:
|
|
1207
|
+
{ name: "password", label: `Password (at least ${S.state.rules.minPassword} characters)`, type: "password", auto: "new-password" },
|
|
1184
1208
|
...(users.length ? [{ name: "off", label: "Remove the login: the site is public again", type: "check", value: false }] : []),
|
|
1185
1209
|
],
|
|
1186
1210
|
ok: "Save",
|
|
@@ -1189,7 +1213,7 @@ function authDialog(n, m) {
|
|
|
1189
1213
|
const user = v.user.trim() || "admin";
|
|
1190
1214
|
const bad = loginError(user, v.password);
|
|
1191
1215
|
if (bad) return invalid(...bad);
|
|
1192
|
-
run(["auth", m.name, ...sv(n), "-u", user, "
|
|
1216
|
+
run(["auth", m.name, ...sv(n), "-u", user, "--password", v.password], { server: n });
|
|
1193
1217
|
},
|
|
1194
1218
|
});
|
|
1195
1219
|
}
|
|
@@ -1235,14 +1259,8 @@ function forget(n) {
|
|
|
1235
1259
|
});
|
|
1236
1260
|
}
|
|
1237
1261
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
await http(`/api/servers/${encodeURIComponent(n)}/default`, {});
|
|
1241
|
-
await refreshState();
|
|
1242
|
-
toast(`${n} is now the default server`);
|
|
1243
|
-
} catch (e) {
|
|
1244
|
-
toast(e.message);
|
|
1245
|
-
}
|
|
1262
|
+
function makeDefault(n) {
|
|
1263
|
+
run(["server", "use", n], { server: n, after: (j) => (j.info.code === 0 ? void refreshState() : undefined) });
|
|
1246
1264
|
}
|
|
1247
1265
|
|
|
1248
1266
|
// ---- views ---------------------------------------------------------------
|
|
@@ -1828,7 +1846,7 @@ function traefikView(n) {
|
|
|
1828
1846
|
fields: [
|
|
1829
1847
|
{ name: "domain", label: "Domain", value: x.traefik.dashboard ?? "", placeholder: x.domains[0] ? `traefik.${x.domains[0]}` : "traefik.example.com", hint: x.traefik.dashboard ? "Empty: take the dashboard off its domain." : "Needs an A record pointing at the server." },
|
|
1830
1848
|
{ name: "user", label: "Login user", value: "admin", placeholder: "admin" },
|
|
1831
|
-
{ name: "password", label:
|
|
1849
|
+
{ name: "password", label: `Login password (at least ${S.state.rules.minPassword} characters)`, type: "password", hint: x.traefik.dashboard ? "Empty: keeps the current one." : "Empty: a generated one, shown in the output." },
|
|
1832
1850
|
],
|
|
1833
1851
|
ok: "Apply",
|
|
1834
1852
|
onOk: (v) => {
|
|
@@ -1879,7 +1897,7 @@ function traefikView(n) {
|
|
|
1879
1897
|
<div class="body">${rows([
|
|
1880
1898
|
["domain", dash.hosts.length ? `${dash.hosts.map(link).join(", ")} <span class="dim">(with its login)</span>` : `<span class="dim">none${foreign ? "" : ": only through the tunnel"}</span>`],
|
|
1881
1899
|
["tunnel", dash.tunnel
|
|
1882
|
-
? `${tunnelBtn(n, dashArgv, "Open via tunnel")} <span class="dim">lf traefik dashboard</span>`
|
|
1900
|
+
? `${tunnelBtn(n, dashArgv, "Open via tunnel")} <span class="dim mono">lf traefik dashboard</span>`
|
|
1883
1901
|
: foreign ? `<span class="dim">route its dashboard (api@internal) to a domain, or run <span class="mono">lf traefik dashboard --port <its api port></span></span>`
|
|
1884
1902
|
: `<span class="dim">set up by an older landfall</span> ${btn("Enable tunnel", () => run(["traefik", "setup", ...sv(n)], reload), "sm", "Re-runs lf traefik setup, which adds it")}`],
|
|
1885
1903
|
])}</div>
|