Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,7 @@ e2e_venv/
web-src/tests/e2e/.run/
web-src/playwright-report/
web-src/test-results/

# Claude personal context (not shared)
CLAUDE.local.md
.claude/
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@

## What's new in this fork

### 2026-06-19 — UI refresh, dark mode and in-app tabs

A visual refresh of the main app, a dark theme, and the ability to keep several
scripts open at once.

- **Dark mode**: a light/dark toggle (top-right of the sidebar, admin header, and
login page) that persists in `localStorage` and otherwise follows the OS
`prefers-color-scheme`. The Vuetify theme is injected into the shared theme
helper, so the Vuetify-less login page reuses the same preference without
bundling Vuetify.
- **Refreshed main app**: server badge + persistent full-width search in the
sidebar, folder groups shown as labelled sections, smaller script names, a play
icon on the Execute button, and a GitHub-style dark console for script output.
A modern system-UI font stack replaces the heavier Roboto look.
- **In-app tabs**: opening scripts now adds them to a persistent tab bar so you
can switch between several without leaving the app. Each tab keeps its entered
parameter values, and running executions stay alive when you switch tabs. Open
tabs are persisted to `localStorage`.

### 2026-06-17 — Code coverage measurement

Both test suites now report coverage and upload it to [Codecov](https://codecov.io/gh/knep/script-server) under separate `python` / `frontend` flags.
Expand Down
10 changes: 9 additions & 1 deletion web-src/src/admin/AdminApp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<v-tab to="/logs">Logs</v-tab>
<v-tab to="/scripts">Scripts</v-tab>
</v-tabs>
<ThemeToggle color="white" class="theme-toggle"/>
</div>
<div v-if="subheader" class="subheader">{{ subheader }}</div>
</div>
Expand All @@ -22,12 +23,13 @@

<script>
import File_upload from '@/common/components/file_upload'
import ThemeToggle from '@/common/components/ThemeToggle'
import {useAdminUiStore} from '@/admin/stores/ui'
import {useAuthStore} from '@/common/stores/auth'

export default {
name: 'AdminApp',
components: {File_upload},
components: {File_upload, ThemeToggle},

mounted() {
useAuthStore().init()
Expand Down Expand Up @@ -95,6 +97,12 @@ export default {
margin: 0 4px;
}

.theme-toggle {
flex-shrink: 0;
margin-left: auto;
margin-right: 8px;
}

.subheader {
font-size: 1em;
font-weight: 400;
Expand Down
5 changes: 5 additions & 0 deletions web-src/src/admin/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import AdminApp from './AdminApp';
import router from './router/router';
import vueDirectives from '@/common/vueDirectives'
import vuetify from '@/common/vuetifyPlugin'
import {initTheme, registerVuetifyTheme} from '@/common/utils/theme'
import {forEachKeyValue} from '@/common/utils/common'

const pinia = createPinia()
Expand All @@ -17,4 +18,8 @@ forEachKeyValue(vueDirectives, (id, definition) => {
})

app.use(pinia).use(router).use(vuetify)

registerVuetifyTheme(vuetify.theme.global)
initTheme()

app.mount('#admin-page')
19 changes: 19 additions & 0 deletions web-src/src/assets/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,25 @@ h6.header {
justify-content: center;
}

.theme-toggle-floating {
position: fixed;
top: 12px;
right: 12px;
width: 40px;
height: 40px;
padding: 0;
border: none;
border-radius: 50%;
background: transparent;
font-size: 18px;
line-height: 40px;
cursor: pointer;
}

.theme-toggle-floating:hover {
background-color: var(--hover-color);
}

#login-panel {
width: 300px;
background-color: var(--background-color-level-4dp);
Expand Down
44 changes: 43 additions & 1 deletion web-src/src/assets/css/shared.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
--background-color-level-16dp: var(--background-color);
--background-color-disabled: rgba(0, 0, 0, 0.12); /* disabled button */

--script-header-background: url('../titleBackground_small.jpg') center left / cover no-repeat;
--script-header-background: var(--background-color);
--login-header-background: url('../titleBackground_login.jpg') center / cover no-repeat;

--separator-color: #DDDDDD; /* borders between components */
Expand All @@ -83,6 +83,48 @@
--outline-color-disabled: var(--background-color-disabled);

--error-color: #F44336;

/* Typography: a modern system-UI stack (replaces the heavier Roboto look)
and a monospace stack for terminal/console output. */
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, system-ui, 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'SF Mono', 'JetBrains Mono', 'Fira Code', Menlo, Consolas, 'Liberation Mono', monospace;
}

/* Apply the sans stack app-wide. !important so it wins over Vuetify's own
.v-application font rule regardless of stylesheet import order. */
body,
.v-application {
font-family: var(--font-sans) !important;
}

/* ── Dark theme ─────────────────────────────────────────────────────────
Toggled by the `theme-dark` class on <html> (see common/utils/theme.js).
Mirrors the Vuetify scriptServerDark theme so custom-styled and Vuetify
components stay consistent. */
html.theme-dark {
--hover-color: rgba(255, 255, 255, 0.06);
--focus-color: rgba(255, 255, 255, 0.12);
--focus-color-solid: #3a3a3a;

--font-color-main: rgba(255, 255, 255, 0.92);
--font-color-medium: rgba(255, 255, 255, 0.62);
--font-color-disabled: rgba(255, 255, 255, 0.40);

--primary-color-light-color: #18403c;
--font-on-primary-color-light--main: rgba(255, 255, 255, 0.92);

--surface-color: #2a2a2a; /* surface color and log panel color */

--background-color: #1a1a1a;
--background-color-slight-emphasis: rgba(255, 255, 255, 0.04);
--background-color-high-emphasis: rgba(255, 255, 255, 0.10);
--background-color-disabled: rgba(255, 255, 255, 0.12);

--separator-color: rgba(255, 255, 255, 0.14);

--outline-color: rgba(255, 255, 255, 0.30);

color-scheme: dark;
}

/* ── Minimal 12-column grid (replaces materialize _grid.scss) ─────────── */
Expand Down
35 changes: 35 additions & 0 deletions web-src/src/common/components/ThemeToggle.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<template>
<v-btn
:icon="dark ? 'brightness_7' : 'brightness_2'"
:title="dark ? 'Switch to light mode' : 'Switch to dark mode'"
variant="text"
:color="color"
density="compact"
@click="toggle"
/>
</template>

<script>
import {isDarkActive, toggleTheme} from '@/common/utils/theme'

export default {
name: 'ThemeToggle',
props: {
color: {
type: String,
default: 'primary'
}
},
data() {
return {
dark: isDarkActive()
}
},
methods: {
toggle() {
toggleTheme()
this.dark = isDarkActive()
}
}
}
</script>
49 changes: 40 additions & 9 deletions web-src/src/common/components/log_panel.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
<template>
<div class="log-panel">
<div class="log-panel-shadow"
<div class="log-panel-header">
<v-icon :size="16">terminal</v-icon>
<span>Output</span>
</div>
<div ref="shadow" class="log-panel-shadow"
v-bind:class="{
'shadow-top': !atTop && atBottom,
'shadow-bottom': atTop && !atBottom,
Expand Down Expand Up @@ -154,7 +158,7 @@ export default {
terminal.addEventListener('mousedown', () => this.mouseDown = true);
terminal.addEventListener('mouseup', () => this.mouseDown = false);

this.$el.insertBefore(terminal, this.$el.children[0]);
this.$el.insertBefore(terminal, this.$refs.shadow);

this.revalidateScroll()
}
Expand Down Expand Up @@ -204,12 +208,36 @@ export default {
position: relative;
min-height: 0;

background: var(--surface-color);
display: flex;
flex-direction: column;

background: #0d1117;

width: 100%;

border: solid 1px var(--separator-color);
border-radius: 2px;
border: solid 1px #30363d;
border-radius: 8px;
overflow: hidden;
}

.log-panel-header {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 6px;

padding: 6px 12px;
background: #161b22;
border-bottom: 1px solid #30363d;

color: #768390;
font-size: 12px;
font-family: var(--font-sans);
user-select: none;
}

.log-panel-header :deep(.v-icon) {
color: #768390;
}

.log-panel-shadow {
Expand Down Expand Up @@ -246,7 +274,7 @@ export default {
}

.log-panel .copy-text-button i {
color: var(--font-color-disabled);
color: #768390;
}

.log-panel .download-text-button {
Expand All @@ -256,19 +284,22 @@ export default {
}

.log-panel .download-text-button i {
color: var(--font-color-disabled);
color: #768390;
}

/*noinspection CssInvalidPropertyValue,CssOverwrittenProperties*/
.log-panel :deep(.log-content) {
display: block;
overflow-y: auto;
height: 100%;
flex: 1 1 0;
min-height: 0;
width: 100%;

color: #adbac7;
font-family: var(--font-mono);
font-size: .875em;

padding: 1.5em;
padding: 1.25em 1.5em;

white-space: pre-wrap; /* CSS 3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
Expand Down
68 changes: 68 additions & 0 deletions web-src/src/common/utils/theme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* Light/dark theme management.
*
* Keeps two things in sync:
* - the `theme-dark` class on <html>, which switches the custom CSS variables
* defined in assets/css/shared.css
* - the active Vuetify theme (scriptServer / scriptServerDark), when an app
* has registered one via registerVuetifyTheme()
*
* Vuetify is injected rather than imported so the (Vuetify-less) login page can
* reuse this module — and respect the same preference — without bundling it.
*
* The preference is persisted in localStorage; when none is stored the OS
* `prefers-color-scheme` is used.
*/

const STORAGE_KEY = 'script_server_theme'

// vuetify.theme.global, set by apps that use Vuetify (main, admin). Left null
// on the login page, where only the <html> class needs toggling.
let vuetifyTheme = null

export function registerVuetifyTheme(themeGlobal) {
vuetifyTheme = themeGlobal
}

function prefersDark() {
return !!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)
}

export function getStoredTheme() {
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored === 'dark' || stored === 'light') {
return stored
}
} catch (e) {
// localStorage may be unavailable (private mode, blocked) — fall through
}
return null
}

export function isDarkActive() {
return document.documentElement.classList.contains('theme-dark')
}

export function applyTheme(name) {
const dark = name === 'dark'
document.documentElement.classList.toggle('theme-dark', dark)
if (vuetifyTheme) {
vuetifyTheme.name.value = dark ? 'scriptServerDark' : 'scriptServer'
}
}

export function initTheme() {
applyTheme(getStoredTheme() || (prefersDark() ? 'dark' : 'light'))
}

export function toggleTheme() {
const next = isDarkActive() ? 'light' : 'dark'
try {
localStorage.setItem(STORAGE_KEY, next)
} catch (e) {
// ignore persistence failures, the in-page toggle still applies
}
applyTheme(next)
return next
}
13 changes: 13 additions & 0 deletions web-src/src/common/vuetifyPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ export default createVuetify({
background: '#FFFFFF',
surface: '#FFFFFF'
}
},
scriptServerDark: {
dark: true,
colors: {
// Teal accent is kept — it reads well on dark surfaces
primary: '#26a69a',
'primary-darken-1': '#00796B',
secondary: '#26a69a',
error: '#FF6B6B',
// Mirror the dark --background-color / --surface-color
background: '#1a1a1a',
surface: '#242424'
}
}
}
},
Expand Down
Loading
Loading