From a02f49e794dc97967da4505a077c41fcd63ef95c Mon Sep 17 00:00:00 2001 From: Uniskela <104075208+uniskela@users.noreply.github.com> Date: Wed, 7 Oct 2026 11:00:23 +0000 Subject: [PATCH 1/2] feat: add stack history and change indicators Expose stack-scoped Git history in the UI and surface draft counts on the Changes tab and stack header so outstanding work is easy to find. Co-authored-by: Cursor --- docs/UX.md | 7 +- docs/getting-started.md | 3 +- docs/index.md | 3 +- src/app/styles/source.css | 93 ++++++ .../stacks/[stackId]/history/page.tsx | 18 ++ .../[workspaceId]/stacks/[stackId]/layout.tsx | 15 +- src/ui/source/changes-view.tsx | 45 +-- src/ui/source/history-view.tsx | 301 ++++++++++++++++++ src/ui/source/stack-tabs.tsx | 47 ++- src/ui/source/unified-diff.tsx | 58 ++++ tests/e2e/source.spec.ts | 137 +++++++- 11 files changed, 673 insertions(+), 54 deletions(-) create mode 100644 src/app/w/[workspaceId]/stacks/[stackId]/history/page.tsx create mode 100644 src/ui/source/history-view.tsx create mode 100644 src/ui/source/unified-diff.tsx diff --git a/docs/UX.md b/docs/UX.md index a2f9545..130e46e 100644 --- a/docs/UX.md +++ b/docs/UX.md @@ -27,15 +27,16 @@ Signed-in operators work inside a workspace. Primary sidebar navigation (desktop **Header:** stack name + optional runtime strip (health, container count, image, last refresh) -**Tabs:** Compose | Files | Docs | Environment | Secrets | Changes | Deployments +**Tabs:** Editor | Docs | Environment | Changes | History | Settings (Secrets and Deployments arrive later) No primary nav items for Containers / Images / Networks / Volumes. Repository pages have **Overview**, **Files**, **Docs** and **Changes** tabs: the same editor, docs and review views scoped to the whole repository, for files outside any stack (root README, `docs/`, shared `.env.example`). -Implemented in PR #3: **Editor** (explorer, tabs, problems, status bar), **Docs**, **Environment**, **Changes** and -**Settings** tabs. Secrets and Deployments arrive with PR #6 and PR #5. +Implemented: **Editor**, **Docs**, **Environment**, **Changes**, **History** (stack-scoped commits at the +fetched snapshot) and **Settings**. Draft counts appear on the Changes tab and stack header when work is outstanding. +Secrets and Deployments arrive with PR #6 and PR #5. ## Desktop diff --git a/docs/getting-started.md b/docs/getting-started.md index 1042ad9..17cd789 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -91,7 +91,8 @@ can't be opened or edited. Drafts are stored in stack-manager's database and never leave the server. If the file changes in Git after you start a draft, the editor warns you, so you don't overwrite someone else's change unknowingly. -Committing drafts, safe push and Git history ship in v0.5.0; see the [roadmap](plans/MVP_PLAN.md). +The **History** tab lists commits that touched the stack at the last fetched snapshot. Committing drafts and safe push +ship in v0.5.0; see the [roadmap](plans/MVP_PLAN.md). ## 5. Files outside stacks diff --git a/docs/index.md b/docs/index.md index e905147..701ba79 100644 --- a/docs/index.md +++ b/docs/index.md @@ -35,7 +35,8 @@ stack-manager looks after the Git source they are deployed from. stack-manager is in early development (0.x). Available today: sign-in, workspaces, encrypted credentials, repository connections, stacks, dashboard and settings, the editor with drafts, docs, the environment inventory and change review. -Coming in v0.5.0: committing drafts, safe push and Git history. After that on the [roadmap](plans/MVP_PLAN.md): +Stack-scoped Git history is available on each stack's History tab. Coming in v0.5.0: committing drafts and safe push. +After that on the [roadmap](plans/MVP_PLAN.md): deploying only the stacks a commit changed (through Portainer and others), then optional read-only runtime feedback. ## Documentation diff --git a/src/app/styles/source.css b/src/app/styles/source.css index 97ec3d4..781107c 100644 --- a/src/app/styles/source.css +++ b/src/app/styles/source.css @@ -1161,6 +1161,99 @@ select.compact-select { font-size: var(--text-xs); font-weight: 600; white-space: nowrap; + text-decoration: none; +} + +.page-title-row .draft-badge { + margin-left: 0; +} + +/* ---------- Stack history ---------- */ +.history-list { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: var(--space-3); +} + +.history-item { + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); + overflow: hidden; +} + +.history-row { + display: flex; + align-items: flex-start; + gap: var(--space-3); + width: 100%; + padding: var(--space-3) var(--space-4); + border: 0; + background: transparent; + color: inherit; + text-align: left; + cursor: pointer; +} + +.history-row:hover, +.history-row:focus-visible { + background: var(--surface-2); +} + +.history-row-main { + display: grid; + gap: var(--space-1); + min-width: 0; + flex: 1; +} + +.history-subject { + font-weight: 600; + overflow-wrap: anywhere; +} + +.history-meta { + display: flex; + flex-wrap: wrap; + gap: 0 0.25rem; +} + +.history-files, +.history-files-empty { + margin: 0; + padding: 0 var(--space-4) var(--space-3); + padding-left: calc(var(--space-4) + 1.5rem); + list-style: none; + display: grid; + gap: var(--space-1); + font-size: var(--text-sm); +} + +.history-files li { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-2); + min-width: 0; +} + +.history-files .mono { + overflow-wrap: anywhere; +} + +.history-detail { + border-top: 1px solid var(--border); +} + +.history-detail-title { + margin: 0; + font-size: var(--text-md); +} + +.history-file-diff + .history-file-diff { + border-top: 1px solid var(--border); } /* Flat lists (grouped by repository only, or not at all) have no folder labels above their first row. */ diff --git a/src/app/w/[workspaceId]/stacks/[stackId]/history/page.tsx b/src/app/w/[workspaceId]/stacks/[stackId]/history/page.tsx new file mode 100644 index 0000000..da011b1 --- /dev/null +++ b/src/app/w/[workspaceId]/stacks/[stackId]/history/page.tsx @@ -0,0 +1,18 @@ +import type { Metadata } from 'next'; +import { getContainer } from '@/server/container'; +import { HistoryView } from '@/ui/source/history-view'; + +export const metadata: Metadata = { title: 'History' }; + +export default async function StackHistoryPage({ + params, +}: { + params: Promise<{ workspaceId: string; stackId: string }>; +}) { + const { workspaceId, stackId } = await params; + const { stacks } = getContainer(); + const stack = await stacks.get(workspaceId, stackId); + return ( + + ); +} diff --git a/src/app/w/[workspaceId]/stacks/[stackId]/layout.tsx b/src/app/w/[workspaceId]/stacks/[stackId]/layout.tsx index 480123e..7a67ea4 100644 --- a/src/app/w/[workspaceId]/stacks/[stackId]/layout.tsx +++ b/src/app/w/[workspaceId]/stacks/[stackId]/layout.tsx @@ -1,4 +1,4 @@ -import { Folder, FolderGit2, GitBranch, GitCommitHorizontal, Hourglass } from 'lucide-react'; +import { Folder, FolderGit2, GitBranch, GitCommitHorizontal, Hourglass, PencilLine } from 'lucide-react'; import Link from 'next/link'; import { getContainer } from '@/server/container'; import { EmptyState } from '@/ui/primitives/empty-state'; @@ -24,6 +24,7 @@ export default async function StackLayout({ stacks.listForRepository(workspaceId, stack.repository.id), ]); const base = `/w/${workspaceId}/stacks/${stackId}`; + const draftLabel = `${drafts} draft${drafts === 1 ? '' : 's'}`; return (
@@ -37,6 +38,18 @@ export default async function StackLayout({ /> } title={stack.name} + status={ + drafts > 0 ? ( + +