mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-14 17:40:37 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Instance settings collect deployment-wide controls; one of them was the Heartbeats page, an instance-wide list of scheduler heartbeat agents with enable/disable toggles > - The same controls live on each agent's own configuration surface, so the standalone list duplicates them, and its framing no longer matches how heartbeat agents are managed > - Keeping a settings view that no longer makes sense costs every deployment navigation noise and maintenance > - This pull request removes the page, its route, its navigation entries, and its hidden-settings key for all deployments > - The benefit is a smaller, coherent settings surface, with operator hidden-settings lists that still mention the retired key continuing to work unchanged ## Linked Issues or Issue Description No public issue exists; describing the issue inline per the enhancement template: **What existing behavior does this improve?** The instance settings surface — specifically the Settings → Heartbeats page, which listed scheduler heartbeat agents instance-wide with enable/disable toggles. The view no longer makes sense as a standalone settings page: the same controls are available on each agent's configuration surface, and the instance-wide list framing does not match how heartbeat agents are managed. **Subsystem affected** Cross-cutting: `ui/` (page, route, navigation), `packages/shared` (settings-visibility registry), docs. **Current behavior** The page renders at `/company/settings/instance/heartbeats`, appears in the settings sidebar and tab bar, and is hideable by hosting operators via the `instance.heartbeats` key of `PAPERCLIP_HIDDEN_SETTINGS`. **Proposed behavior** The page, route, and navigation entries are removed for every deployment. The `instance.heartbeats` registry key is retired; operator lists that still send it are logged and ignored, so mixed-version fleets keep working. Remembered settings paths pointing at the old page remap to the settings root. Heartbeat APIs are unchanged. **Reason and benefit** A smaller, coherent settings surface with no duplicated controls; less navigation noise and maintenance for every deployment. **Breaking changes** None functional. Bookmarks and remembered paths to the removed page land on the settings root; `PAPERCLIP_HIDDEN_SETTINGS` lists that still include `instance.heartbeats` log a warning and are otherwise honored unchanged. ## What Changed - Deleted `ui/src/pages/InstanceSettings.tsx` (the Heartbeats view) and its route in `ui/src/App.tsx`. - Removed the sidebar entry (`CompanySettingsSidebar`) and tab-bar item (`CompanySettingsNav`). - Removed `"/heartbeats"` from the remembered-settings-path allowlist; remembered heartbeats paths now remap to the settings root. - Retired the `instance.heartbeats` key from the shared settings-visibility registry and the environment-variables doc; documented that retired keys are ignored with a warning. - Dropped the now-unused UI client wrapper for the instance scheduler-agent list (`heartbeatsApi.listInstanceSchedulerAgents`); the server endpoint stays. - Removed the unused `schedulerHeartbeats` query key. ## Verification - `npx vitest run packages/shared/src/settings-visibility.test.ts ui/src/lib/instance-settings.test.ts ui/src/components/CompanySettingsSidebar.test.tsx ui/src/components/access/CompanySettingsNav.test.tsx` — 24 tests passing. - Full `ui` vitest suite: 4426 tests, 4 failures — all in files this PR does not touch; 3 were load-induced timeouts that pass on rerun, and `OnboardingWizard.test.tsx` "renders instead of throwing when the browser denies storage access" fails identically on a clean master checkout (pre-existing). - `pnpm --filter @paperclipai/ui typecheck` and `pnpm --filter @paperclipai/shared typecheck` — clean. - Merged `master` to clear a conflict (see below) and re-ran the four focused suites (24 passing), `ui/src/App.test.tsx` and `ui/src/plugins/bridge.test.ts` (22 passing), and both typechecks — all clean. Full CI is green on the merge commit. ## Merge With master `master` gained the `company` → `organization` copy pass (#12243), which reworded strings inside `ui/src/pages/InstanceSettings.tsx` — the page this branch deletes — producing a modify/delete conflict. Resolved by keeping the deletion: the page is going away, so the rewording of its copy has nothing to apply to. Every other file merged cleanly, and `master`'s rewording in `App.tsx`, `App.test.tsx`, and `CompanySettingsSidebar.tsx` sits away from this branch's structural removals, so both changes survive. The net diff against `master` is unchanged from the pre-merge review: the same 13 files, 23 insertions, 330 deletions. ## Risks - Low. Pure removal of a UI surface; heartbeat data and APIs are untouched. Operators still listing `instance.heartbeats` in `PAPERCLIP_HIDDEN_SETTINGS` get a warning log and otherwise unchanged behavior (covered by the registry's unknown-key handling). Bookmarks and remembered paths to the old page land on the settings root. ## Model Used Claude (Anthropic), model id `claude-fable-5`, extended thinking, agentic tool use via Claude Code. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge
101 lines
3.4 KiB
TypeScript
101 lines
3.4 KiB
TypeScript
export const INSTANCE_SETTINGS_PATH_PREFIX = "/company/settings/instance";
|
|
export const DEFAULT_INSTANCE_SETTINGS_PATH = `${INSTANCE_SETTINGS_PATH_PREFIX}/general`;
|
|
|
|
const LEGACY_INSTANCE_SETTINGS_PATH_PREFIX = "/instance/settings";
|
|
const LEGACY_SETTINGS_PATH_PREFIX = "/settings";
|
|
|
|
function splitPath(rawPath: string): { pathname: string; search: string; hash: string } {
|
|
const match = rawPath.match(/^([^?#]*)(\?[^#]*)?(#.*)?$/);
|
|
return {
|
|
pathname: match?.[1] ?? rawPath,
|
|
search: match?.[2] ?? "",
|
|
hash: match?.[3] ?? "",
|
|
};
|
|
}
|
|
|
|
function normalizePathForMatching(rawPath: string): { pathname: string; search: string; hash: string } {
|
|
const { pathname, search, hash } = splitPath(rawPath);
|
|
const segments = pathname.split("/").filter(Boolean);
|
|
const first = segments[0]?.toLowerCase();
|
|
const second = segments[1]?.toLowerCase();
|
|
|
|
if (first === "company") {
|
|
return { pathname: `/${segments.join("/")}`, search, hash };
|
|
}
|
|
|
|
if (second === "company" || second === "settings" || second === "instance") {
|
|
return { pathname: `/${segments.slice(1).join("/")}`, search, hash };
|
|
}
|
|
|
|
return { pathname, search, hash };
|
|
}
|
|
|
|
function instanceSettingsSuffix(pathname: string): string | null {
|
|
if (pathname === "/company/settings/environments") return "/environments";
|
|
|
|
if (pathname === INSTANCE_SETTINGS_PATH_PREFIX) return "/general";
|
|
if (pathname.startsWith(`${INSTANCE_SETTINGS_PATH_PREFIX}/`)) {
|
|
return pathname.slice(INSTANCE_SETTINGS_PATH_PREFIX.length);
|
|
}
|
|
|
|
if (pathname === LEGACY_INSTANCE_SETTINGS_PATH_PREFIX || pathname === "/instance") {
|
|
return "/general";
|
|
}
|
|
if (pathname.startsWith(`${LEGACY_INSTANCE_SETTINGS_PATH_PREFIX}/`)) {
|
|
return pathname.slice(LEGACY_INSTANCE_SETTINGS_PATH_PREFIX.length);
|
|
}
|
|
|
|
if (pathname === LEGACY_SETTINGS_PATH_PREFIX) return "/general";
|
|
if (pathname.startsWith(`${LEGACY_SETTINGS_PATH_PREFIX}/`)) {
|
|
return pathname.slice(LEGACY_SETTINGS_PATH_PREFIX.length);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function normalizeRememberedInstanceSettingsPath(rawPath: string | null): string {
|
|
if (!rawPath) return DEFAULT_INSTANCE_SETTINGS_PATH;
|
|
|
|
const { pathname, search, hash } = normalizePathForMatching(rawPath);
|
|
const suffix = instanceSettingsSuffix(pathname);
|
|
if (!suffix) return DEFAULT_INSTANCE_SETTINGS_PATH;
|
|
|
|
if (
|
|
suffix === "/profile" ||
|
|
suffix === "/general" ||
|
|
suffix === "/environments" ||
|
|
suffix === "/access" ||
|
|
suffix === "/plugins" ||
|
|
suffix === "/experimental" ||
|
|
suffix === "/adapters"
|
|
) {
|
|
return `${INSTANCE_SETTINGS_PATH_PREFIX}${suffix}${search}${hash}`;
|
|
}
|
|
|
|
if (/^\/plugins\/[^/?#]+$/.test(suffix)) {
|
|
return `${INSTANCE_SETTINGS_PATH_PREFIX}${suffix}${search}${hash}`;
|
|
}
|
|
|
|
return DEFAULT_INSTANCE_SETTINGS_PATH;
|
|
}
|
|
|
|
/**
|
|
* Remaps a remembered instance-settings path onto the settings root when its
|
|
* page is hidden by the hosting operator (`hidden` holds keys from the shared
|
|
* settings-visibility registry). Saves a redirect hop; the route-level
|
|
* HiddenSettingsPageGate stays the enforcement point.
|
|
*/
|
|
export function filterHiddenInstanceSettingsPath(
|
|
normalizedPath: string,
|
|
hidden: ReadonlySet<string>,
|
|
): string {
|
|
const { pathname } = splitPath(normalizedPath);
|
|
const suffix = instanceSettingsSuffix(pathname);
|
|
if (!suffix) return normalizedPath;
|
|
const page = suffix.split("/")[1];
|
|
if (page && page !== "general" && hidden.has(`instance.${page}`)) {
|
|
return DEFAULT_INSTANCE_SETTINGS_PATH;
|
|
}
|
|
return normalizedPath;
|
|
}
|