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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# CHANGELOG

## v2.3.0 (25 September 2026)

### Administration UI

- Unified every Svelte administration route around shared Carbon page shells, section panels, resource states, authentication layouts, and confirmations
- Reorganized Settings into focused tabs and rebuilt Policies, Devices, DNS, HTTPS inspection, AI image filtering, Proxy users, Overview, Stats, Logs, and Services with consistent hierarchy and feedback
- Added explicit loading, empty, error, retry, save, and destructive-action states while removing native confirmations and legacy route-local layout patterns
- Made dense tables and editors own their overflow, improved tablet and phone layouts, and fixed the compact navigation drawer intercepting pointer or keyboard input after it closes
- Added a page-not-found recovery state, base-path-aware navigation and certificate downloads, and clearer guidance around HTTPS inspection, external AI processing, proxy users, and device-policy coverage

### Quality

- Expanded focused UI contracts for the application shell, connected settings, setup, policies, devices, DNS/HTTPS inspection, AI, and Proxy users
- Eliminated the accepted Svelte diagnostic baseline: the UI now checks with zero errors and zero warnings
- Rebuilt and validated the embedded administration assets

## v2.2.2 (24 September 2026)

### Performance
Expand Down
169 changes: 89 additions & 80 deletions application/webserver/frontend/files/fs/bundle.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions application/webserver/frontend/files/fs/style-6dc8a68e.css

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion application/webserver/frontend/files/fs/style-dd15640b.css

This file was deleted.

2 changes: 1 addition & 1 deletion application/webserver/frontend/files/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Gatesentry</title>
<script type="module" crossorigin src="/fs/bundle.js"></script>
<link rel="stylesheet" href="/fs/style-dd15640b.css">
<link rel="stylesheet" href="/fs/style-6dc8a68e.css">
</head>
<body>
<div id="app"></div>
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ var GSPROXYPORT = "10413"
var GSWEBADMINPORT = "10786"
var GSBASEDIR = ""
var Baseendpointv2 = "https://www.gatesentryfilter.com/api/"
var GATESENTRY_VERSION = "2.2.2"
var GATESENTRY_VERSION = "2.3.0"
var GS_BOUND_ADDRESS = ":"
var R *application.GSRuntime

Expand Down
280 changes: 127 additions & 153 deletions ui/src/App.svelte
Original file line number Diff line number Diff line change
@@ -1,191 +1,165 @@
<script lang="ts">
import {
Header,
SkipToContent,
Content,
SideNav,
} from "carbon-components-svelte";
import { Content, SideNav } from "carbon-components-svelte";
import { Router, Route } from "svelte-routing";
import { init, register } from "svelte-i18n";
import { onMount } from "svelte";
import { store } from "./store/apistore";
import { getBasePath, gsNavigate } from "./lib/navigate";
import Globalheader from "./components/globalheader.svelte";
import Sidenavmenu from "./components/sidenavmenu.svelte";
import Notifications from "./components/notifications.svelte";
import AuthShell from "./components/layout/AuthShell.svelte";
import ResourceState from "./components/layout/ResourceState.svelte";
import Login from "./routes/login/login.svelte";
import Setup from "./routes/setup/setup.svelte";
import Logs from "./routes/logs/logs.svelte";
import Headermenu from "./components/headermenu.svelte";
import Sidenavmenu from "./components/sidenavmenu.svelte";
import Headerrightnav from "./components/headerrightnav.svelte";
import { store } from "./store/apistore";
import { gsNavigate, getBasePath } from "./lib/navigate";
import Filter from "./routes/filter/filter.svelte";
import Notifications from "./components/notifications.svelte";
import Settings from "./routes/settings/settings.svelte";
import Services from "./routes/services/services.svelte";
import Home from "./routes/home/home.svelte";
import Dns from "./routes/dns/dns.svelte";
import Stats from "./routes/stats/stats.svelte";
import AI from "./routes/ai/ai.svelte";

import { register, init, _ } from "svelte-i18n";
import Users from "./routes/users/users.svelte";
import Globalheader from "./components/globalheader.svelte";
import Rules from "./routes/rules/rules.svelte";
import Devices from "./routes/devices/devices.svelte";
import NotFound from "./routes/notfound/notfound.svelte";

export let url = "";

let loaded = false;
async function setup() {
register("en", () => import("./language/en.json"));
type AppState = "checking" | "setup" | "login" | "authenticated" | "error";

await Promise.allSettled([
// TODO: add some more stuff you want to init ...
init({ initialLocale: "en", fallbackLocale: "en" }),
]);
loaded = true;
return true;
}
register("en", () => import("./language/en.json"));
const localization = init({ initialLocale: "en", fallbackLocale: "en" });

const setupResult = setup();
const navigationBreakpoint = 1056;

let state: AppState = "checking";
let isSideNavOpen = false;
let userProfilePanelOpen = false;
let tokenVerified = false;
let tokenChecking = false;
let setupChecked = false;
let setupChecking = false;
let setupStatusFailed = false;
let setupRequired = false;
// setupI18n();

$: loggedIn = $store.api.loggedIn;
let pathname = "/";
let windowWidth = 0;

function checkSetupStatus() {
if (setupChecking) return;
setupStatusFailed = false;
setupChecking = true;
fetch(getBasePath() + "/api/setup/status")
.then((response) => {
if (!response.ok) throw new Error("setup status failed");
return response.json();
})
.then((status) => {
setupRequired = !status.complete;
setupChecked = true;
if (setupRequired) {
store.logout();
gsNavigate("/setup");
}
})
.catch(() => {
// Leave routing in a stable fail-closed state. Only the explicit retry
// below starts another request.
setupStatusFailed = true;
})
.finally(() => setupChecking = false);
}
$: isCompactNavigation = windowWidth < navigationBreakpoint;
$: sideNavVisible = !isCompactNavigation || isSideNavOpen;

$: {
if (loaded) {
if (!setupChecked && !setupChecking && !setupStatusFailed) {
checkSetupStatus();
}
if (setupChecked && !setupRequired && !tokenVerified && !tokenChecking) {
tokenChecking = true;
$store.api.verifyToken().then((isValid) => {
tokenVerified = true;
if (isValid) {
store.refresh();
} else if (setupChecked && !setupRequired) {
gsNavigate("/login");
}
}).finally(() => tokenChecking = false);
const routePath = () => {
const base = getBasePath();
const browserPath = typeof window === "undefined" ? url : window.location.pathname;
if (base && (browserPath === base || browserPath.startsWith(base + "/"))) {
return browserPath.slice(base.length) || "/";
}
return browserPath || "/";
};

const syncPathname = () => {
pathname = routePath();
isSideNavOpen = false;
};

const closeSideNavigation = () => {
isSideNavOpen = false;
if (isCompactNavigation && typeof window !== "undefined") {
window.requestAnimationFrame(() => {
document
.querySelector<HTMLButtonElement>(".bx--header__menu-toggle")
?.focus();
});
}
};

async function checkSession() {
state = "checking";
try {
const response = await fetch(getBasePath() + "/api/setup/status");
if (!response.ok) throw new Error("setup status failed");
const status = await response.json();
if (!status.complete) {
store.logout();
state = "setup";
if (routePath() !== "/setup") gsNavigate("/setup", { replace: true });
return;
}

if (setupChecked && tokenVerified && !setupRequired && !loggedIn) {
gsNavigate("/login");
const valid = await $store.api.verifyToken();
if (valid) {
store.refresh();
state = "authenticated";
if (routePath() === "/login" || routePath() === "/setup") {
gsNavigate("/", { replace: true });
}
} else {
state = "login";
if (routePath() !== "/login") gsNavigate("/login", { replace: true });
}
} catch {
state = "error";
}
}

// afterUpdate(() => {
// if (!loggedIn) {
// navigate("/login");
// }
// });
// onMount(() => {});
onMount(() => {
syncPathname();
window.addEventListener("popstate", syncPathname);
return () => window.removeEventListener("popstate", syncPathname);
});

// $: {
// if (setupResult) {

// }
// }
Promise.resolve(localization).then(checkSession).catch(() => (state = "error"));
</script>

<Router {url} basepath={getBasePath()}>
{#await setupResult}
Loading...
{:then}
{#if setupStatusFailed}
<main class="setup-status-error">
<p role="alert">Unable to read GateSentry setup status.</p>
<button type="button" on:click={checkSetupStatus}>Retry status check</button>
</main>
{:else}
<Globalheader bind:isSideNavOpen bind:userProfilePanelOpen />

<SideNav bind:isOpen={isSideNavOpen} rail>
<Sidenavmenu />
</SideNav>
<svelte:window bind:innerWidth={windowWidth} />

<Content>
<div>
<Route path="/login" component={Login} />
<Route path="/setup" component={Setup} />
<Route path="/dns" component={Dns}></Route>
<Router {url} basepath={getBasePath()}>
{#if state === "checking"}
<AuthShell title="GateSentry" description="Checking this installation…">
<ResourceState state="loading" message="Checking setup and session status" />
</AuthShell>
{:else if state === "error"}
<AuthShell title="GateSentry" description="The administration interface is unavailable.">
<ResourceState
state="error"
title="Unable to read GateSentry setup status."
message="Check that the GateSentry service is running, then try again."
retryLabel="Retry status check"
onRetry={checkSession}
/>
</AuthShell>
{:else if state === "setup"}
<Route path="/setup" component={Setup} />
{:else if state === "login"}
<Route path="/login">
<Login on:authenticated={() => (state = "authenticated")} />
</Route>
{:else}
<div class="app-frame">
<Globalheader bind:isSideNavOpen bind:userProfilePanelOpen {pathname} />
<SideNav
bind:isOpen={isSideNavOpen}
rail
expansionBreakpoint={navigationBreakpoint}
ariaLabel="Primary navigation"
aria-hidden={!sideNavVisible}
inert={!sideNavVisible}
>
<Sidenavmenu {pathname} onNavigate={closeSideNavigation} />
</SideNav>
<Content id="main-content">
<Route path="/dns" component={Dns} />
<Route path="/logs" component={Logs} />
<Route path="/settings">
<Settings />
</Route>
<Route path="/blockedkeywords">
<Filter type="blockedkeywords" />
</Route>
<Route path="/blockedfiletypes">
<Filter type="blockedfiletypes" />
</Route>
<Route path="/excludeurls">
<Filter type="excludeurls" />
</Route>
<Route path="/excludehosts">
<Filter type="excludehosts" />
</Route>
<Route path="/services">
<Services />
</Route>
<Route path="/rules">
<Rules />
</Route>
<Route path="/devices">
<Devices />
</Route>
<Route path="/stats">
<Stats />
</Route>
<Route path="/ai">
<AI />
</Route>
<Route path="/users">
<Users />
</Route>
<Route path="/settings" component={Settings} />
<Route path="/blockedkeywords"><Filter type="blockedkeywords" /></Route>
<Route path="/blockedfiletypes"><Filter type="blockedfiletypes" /></Route>
<Route path="/excludeurls"><Filter type="excludeurls" /></Route>
<Route path="/excludehosts"><Filter type="excludehosts" /></Route>
<Route path="/services" component={Services} />
<Route path="/rules" component={Rules} />
<Route path="/devices" component={Devices} />
<Route path="/stats" component={Stats} />
<Route path="/ai" component={AI} />
<Route path="/users" component={Users} />
<Route path="/" component={Home} />
</div>

<Route component={NotFound} />
</Content>
<Notifications />
</Content>
{/if}
{:catch error}
<!-- <p style="color: red">{error.message}</p> -->
Error: Unable to load localization.
{/await}
</div>
{/if}
</Router>

<style>
.setup-status-error { max-width: 32rem; margin: 15vh auto; padding: 1rem; }
.setup-status-error p { margin-bottom: 1rem; }
</style>
Loading
Loading