Show the running version next to the brand; bump to 0.2.1

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Aleksandr BogomiakovandClaude committed 2026-10-07 15:32:20 +01:00
1 parent 6a55988671
commit 5ccf2b1108
9 files changed
+31 -8

No files matched your search

Generated
+1 -1
View File
@@ -1794,7 +1794,7 @@ dependencies = [
[[package]]
name = "meguri"
version = "0.2.0"
version = "0.2.1"
dependencies = [
"argon2",
"base64 0.23.1",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "meguri"
version = "0.2.0"
version = "0.2.1"
edition = "2024"
rust-version = "1.97"
description = "Provider-independent on-call scheduling and configuration control plane"
+2 -2
View File
@@ -2,6 +2,6 @@ apiVersion: v2
name: meguri
description: On-call schedules, replacements and escalation policies as a source of truth
type: application
version: 0.2.0
appVersion: "0.2.0"
version: 0.2.1
appVersion: "0.2.1"
kubeVersion: ">=1.29.0-0"
+2 -2
View File
@@ -285,10 +285,10 @@ async fn dispatch(request: Request) -> Result<Response> {
json!({"oidc":config.oidc.is_some(),"password":config.password_form_available()});
return match identity {
Ok(i) => response(
json!({"user":i.user,"csrf":i.csrf,"auth":auth,"demo":crate::demo_runtime::active(),"demo_lifecycle":crate::demo_runtime::notice()}),
json!({"version":env!("CARGO_PKG_VERSION"),"user":i.user,"csrf":i.csrf,"auth":auth,"demo":crate::demo_runtime::active(),"demo_lifecycle":crate::demo_runtime::notice()}),
),
Err(Error::Unauthorized) => response(
json!({"user":null,"auth":auth,"demo":crate::demo_runtime::active(),"demo_lifecycle":crate::demo_runtime::notice()}),
json!({"version":env!("CARGO_PKG_VERSION"),"user":null,"auth":auth,"demo":crate::demo_runtime::active(),"demo_lifecycle":crate::demo_runtime::notice()}),
),
Err(e) => Err(e),
};
+4
View File
@@ -220,3 +220,7 @@ a.button{display:inline-block;text-decoration:none;border:1px solid #dbe3de;bord
.event-details{display:flex;flex-wrap:wrap;gap:4px 18px;margin:6px 0 0}.event-details>div{min-width:0;max-width:100%}.event-details dt{display:inline;color:var(--muted);font-size:11px}.event-details dt::after{content:": "}.event-details dd{display:inline;margin:0;overflow-wrap:anywhere;word-break:break-word;font-size:12px}
.chips{display:inline-flex;flex-wrap:wrap;gap:4px}.chip{font:11px/1.4 Consolas,monospace;background:#eef3f0;padding:1px 6px;border-radius:10px;overflow-wrap:anywhere}
@media(max-width:700px){.event-search{flex-wrap:wrap}.event-search input{flex-basis:100%}.event-time{margin-left:0;flex-basis:100%}}
.brand .app-version{align-self:flex-end;margin-bottom:7px;font-size:11px;font-weight:500;letter-spacing:0;color:var(--muted)}
@media(max-width:950px){.sidebar .brand{flex-direction:column;gap:2px}.sidebar .brand .app-version{align-self:center;margin:0}}
@media(max-width:700px){.sidebar{gap:4px;padding:8px}.sidebar nav :is(button,a){padding:8px 4px}.sidebar .brand .app-version{margin:0;font-size:10px}}
+2 -2
View File
@@ -18,7 +18,7 @@ document.addEventListener('alpine:init', () => {
}));
Alpine.data('meguri', () => ({
...generationUi(),
user: null, csrf: '', ready: false, busy: false, error: '', toast: '', demo: false, demoLifecycle:null, auth:{oidc:false,password:true}, loginError:null, admin:null, events:[], eventsMore:false, eventSearch:'', eventsLoading:false,
user: null, csrf: '', version: '', ready: false, busy: false, error: '', toast: '', demo: false, demoLifecycle:null, auth:{oidc:false,password:true}, loginError:null, admin:null, events:[], eventsMore:false, eventSearch:'', eventsLoading:false,
loginForm: { username: '', password: '' },
directoryState: { workspaces: [] }, selected: '', detail: null, calendar: { days: [], assignments: [] },
view: 'calendar', scheduleId: '', startDay: '', modal: '', form: {}, menu: false,
@@ -131,7 +131,7 @@ document.addEventListener('alpine:init', () => {
this.consumeLoginError();
{const query=new URLSearchParams(location.search);this.eventSearch=(query.get('ref')||query.get('search')||'').slice(0,200);}
const initial=this.readNavigation();this.selected=initial.workspace;this.view=initial.view;window.addEventListener('popstate',()=>this.restoreNavigation());
await this.run(async () => { const session = await this.api('/api/session'); this.user = session.user; this.csrf = session.csrf || ''; this.auth = session.auth || {oidc:false,password:true}; this.demo = session.demo;this.demoLifecycle=session.demo_lifecycle; if (this.user) { if(this.view==='instance' && !this.user.instance_admin)this.view='calendar'; await this.loadWorkspaces(); if(this.view==='instance')await this.loadAdmin(); } });
await this.run(async () => { const session = await this.api('/api/session'); this.version = session.version || ''; this.user = session.user; this.csrf = session.csrf || ''; this.auth = session.auth || {oidc:false,password:true}; this.demo = session.demo;this.demoLifecycle=session.demo_lifecycle; if (this.user) { if(this.view==='instance' && !this.user.instance_admin)this.view='calendar'; await this.loadWorkspaces(); if(this.view==='instance')await this.loadAdmin(); } });
if(this.setupView)this.setupOpen=true;this.ready = true;this.writeNavigation(true);this.$nextTick(()=>{const update=()=>document.documentElement.style.setProperty('--demo-offset',(this.$refs.demoBanner?.getBoundingClientRect().height||0)+'px');new ResizeObserver(update).observe(this.$refs.demoBanner);update();});if(this.view==='configuration')this.loadGenerationDraft();
setInterval(() => { if (this.user && this.workspace && this.view!=='configuration' && !this.modal && !this.busy && !this.draggedRoute) this.run(() => this.loadWorkspace()); }, 20000);
},
+2
View File
@@ -33,6 +33,7 @@
<main class="login" x-cloak x-show="ready && !user">
<div class="login-story">
<a class="brand" href="/"><span x-text="t('symbol_4d82f1')"></span> <span><span x-text="t('meguri_d160e8')"></span></span>
<small class="app-version" x-show="version" x-text="t('app_version_label',{version})"></small>
</a>
<div>
<span class="eyebrow"><span x-text="t('on_call_in_good_hands_f1cf2d')"></span></span>
@@ -70,6 +71,7 @@
<div class="shell" :inert="!!modal" x-cloak x-show="ready && user">
<aside class="sidebar">
<a class="brand" href="/"><span x-text="t('symbol_4d82f1')"></span> <span><span x-text="t('meguri_d160e8')"></span></span>
<small class="app-version" x-show="version" x-text="t('app_version_label',{version})"></small>
</a>
<span class="eyebrow workspace-label"><span x-text="t('workspace_703988')"></span></span>
<select class="workspace-select" :aria-label="t('team_218887')" x-model="selected" :disabled="busy" @change="changeWorkspace()">
+5
View File
@@ -6398,5 +6398,10 @@
"en": "Issuer",
"ru": "Провайдер",
"fr": "Émetteur"
},
"app_version_label": {
"en": "v{version}",
"ru": "v{version}",
"fr": "v{version}"
}
}
+12
View File
@@ -49,6 +49,10 @@ fs.mkdirSync('test-results', { recursive: true });
assert.equal(await guest.getByRole('link', { name: 'Sign in with SSO' }).isVisible(), !!session.auth.oidc);
const registration = await guest.request.post(base + '/api/register', { headers: { Origin: base }, data: { username: 'intruder1', password: 'long-enough-password', display_name: 'X' } });
assert([404, 405].includes(registration.status()), 'Registration endpoint is gone');
// The running version sits next to the brand on the login screen and on every page after sign-in.
const label = `v${session.version}`;
assert.match(session.version, /^\d+\.\d+\.\d+/);
assert.equal((await guest.locator('.login .brand .app-version').innerText()).trim(), label);
await guest.locator('[data-demo-account=alice]').click();
assert.equal(await guest.getByLabel('Username', { exact: true }).inputValue(), 'alice');
assert.equal(await guest.getByLabel('Password', { exact: true }).inputValue(), secret);
@@ -123,11 +127,19 @@ fs.mkdirSync('test-results', { recursive: true });
await byReference.goto(`${base}/?view=instance&ref=${username}`);
await byReference.locator('#instance-events [data-event-action="user.created"]').first().waitFor();
assert.equal(await byReference.locator('#instance-events input[type=search]').inputValue(), username);
assert.equal((await byReference.locator('.sidebar .brand .app-version').innerText()).trim(), label, 'Version on the instance page');
for (const view of ['calendar', 'requests', 'activity', 'settings']) {
await byReference.goto(`${base}/?view=${view}`);
await byReference.waitForFunction(() => Alpine.$data(document.body).ready);
assert.equal((await byReference.locator('.sidebar .brand .app-version').innerText()).trim(), label, `Version on ${view}`);
}
const mobile = await open({ width: 390, height: 844 });
await signIn(mobile, 'admin');
await mobile.goto(`${base}/?view=instance`);
await mobile.locator('#instance-events .event-row').first().waitFor();
assert(await mobile.evaluate(() => document.documentElement.scrollWidth <= innerWidth), 'Event log must not overflow on mobile');
assert.equal(await mobile.locator('.sidebar .brand .app-version').isVisible(), true, 'The version stays visible on the narrow rail');
await mobile.screenshot({ path: 'test-results/access-version-mobile.png' });
const forbidden = await (await apiLogin('bob')).call('GET', '/api/admin/events');
assert.equal(forbidden.status, 403);
assert.equal((await (await apiLogin('alice')).call('GET', '/api/admin/events?search=x')).status, 403);