CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / TypeScript typecheck (push) Failing after 31s
CI & Build / integration (push) Successful in 48s
CI & Build / Python tests (push) Successful in 1m33s
CI & Build / Build & push image (push) Skipped
Step 7's actual UI. Before this the frontend had zero lesson code — the kind existed for agents only, which is rule 27 failing. THE EDITOR ASKS FOR THE TRIGGER BY NAME, and leads with it. Three fields — the trigger, the claim, the detail — never one markdown box. That is the design step 1 settled, and the evidence is blunt: the snippet corpus carries a trigger on every record with no guard anywhere, because a service composes the title from a named parameter. What is at 100% is a named structured field, not a writer remembering a convention. The trigger gets the most room, its own explanation, and a save button that refuses without it and says why. The form shows the composed title live, so the writer is agreeing to a document they can read rather than one assembled out of sight. A 409 from the duplicate gate is rendered as the record that already covers the moment, with a link to improve it and an explicit override — not as a failure. THE BROWSE VOCABULARY GAINS THE KIND, which #3161 warned this step not to get wrong: a facet chip, a badge label, and routing to `/lessons/:id` rather than the note editor, which cannot edit a trigger. The badge is neutral alongside snippet and process — a hue would make the softest record in the corpus look like the loudest, next to a rule that actually binds. BOTH DIRECTIONS OF THE PROVENANCE. The detail page resolves `learned_from` to titles rather than bare ids, because "#4181" tells a reader nothing about whether it is worth opening. And `LessonsTaughtPanel` answers the reverse on the record's own page — the direction the task body calls the one that gets forgotten. It has no author to type it, which is exactly why it tends never to get built. A component, not markup in the task editor, so the same panel mounts on any record a lesson can cite instead of being written a second time (#3207). Silent when empty: most records taught no lesson, and a panel that says "None yet" everywhere is one people learn to skip. GLOBAL-BY-DEFAULT IS MADE LEGIBLE. A lesson meeting you on a project it was not written on reads as a bug unless the page says otherwise, so the origin line says it as a property of the kind rather than as an apology. Design system tokens throughout; no new raw hex. `--fs-error` rather than `--fs-danger` — 31 uses against 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
191 lines
5.1 KiB
TypeScript
191 lines
5.1 KiB
TypeScript
import { createRouter, createWebHistory } from "vue-router";
|
|
import { useAuthStore } from "@/stores/auth";
|
|
|
|
const router = createRouter({
|
|
history: createWebHistory(),
|
|
routes: [
|
|
{
|
|
// The dashboard ("what to work on") is the landing page; Knowledge
|
|
// remains as the exhaustive "Browse" surface.
|
|
path: "/",
|
|
redirect: "/dashboard",
|
|
},
|
|
{
|
|
path: "/dashboard",
|
|
name: "dashboard",
|
|
component: () => import("@/views/DashboardView.vue"),
|
|
},
|
|
{
|
|
path: "/knowledge",
|
|
name: "knowledge",
|
|
component: () => import("@/views/KnowledgeView.vue"),
|
|
},
|
|
{
|
|
path: "/login",
|
|
name: "login",
|
|
component: () => import("@/views/LoginView.vue"),
|
|
meta: { public: true },
|
|
},
|
|
{
|
|
path: "/register",
|
|
name: "register",
|
|
component: () => import("@/views/RegisterView.vue"),
|
|
meta: { public: true },
|
|
},
|
|
{
|
|
path: "/forgot-password",
|
|
name: "forgot-password",
|
|
component: () => import("@/views/ForgotPasswordView.vue"),
|
|
meta: { public: true },
|
|
},
|
|
{
|
|
path: "/reset-password",
|
|
name: "reset-password",
|
|
component: () => import("@/views/ResetPasswordView.vue"),
|
|
meta: { public: true },
|
|
},
|
|
{
|
|
path: "/register-invite",
|
|
name: "register-invite",
|
|
component: () => import("@/views/RegisterInviteView.vue"),
|
|
meta: { public: true },
|
|
},
|
|
{
|
|
path: "/notes",
|
|
redirect: "/knowledge",
|
|
},
|
|
{
|
|
path: "/notes/new",
|
|
name: "note-new",
|
|
component: () => import("@/views/NoteEditorView.vue"),
|
|
},
|
|
{
|
|
path: "/notes/:id",
|
|
name: "note-view",
|
|
component: () => import("@/views/NoteViewerView.vue"),
|
|
},
|
|
{
|
|
path: "/notes/:id/edit",
|
|
name: "note-edit",
|
|
component: () => import("@/views/NoteEditorView.vue"),
|
|
},
|
|
{
|
|
// Lessons have no list view of their own: the Knowledge browse surface
|
|
// is where every kind is enumerated, and a second list would be a
|
|
// second vocabulary to keep in step with it (#3128's defect, in
|
|
// advance). `/knowledge?type=lesson` is the list.
|
|
path: "/lessons/new",
|
|
name: "lesson-new",
|
|
component: () => import("@/views/LessonEditorView.vue"),
|
|
},
|
|
{
|
|
path: "/lessons/:id",
|
|
name: "lesson-view",
|
|
component: () => import("@/views/LessonDetailView.vue"),
|
|
},
|
|
{
|
|
path: "/lessons/:id/edit",
|
|
name: "lesson-edit",
|
|
component: () => import("@/views/LessonEditorView.vue"),
|
|
},
|
|
{
|
|
path: "/snippets",
|
|
name: "snippets",
|
|
component: () => import("@/views/SnippetListView.vue"),
|
|
},
|
|
{
|
|
path: "/snippets/new",
|
|
name: "snippet-new",
|
|
component: () => import("@/views/SnippetEditorView.vue"),
|
|
},
|
|
{
|
|
path: "/snippets/:id",
|
|
name: "snippet-view",
|
|
component: () => import("@/views/SnippetDetailView.vue"),
|
|
},
|
|
{
|
|
path: "/snippets/:id/edit",
|
|
name: "snippet-edit",
|
|
component: () => import("@/views/SnippetEditorView.vue"),
|
|
},
|
|
{
|
|
path: "/graph",
|
|
name: "graph",
|
|
component: () => import("@/views/GraphView.vue"),
|
|
},
|
|
{
|
|
path: "/projects",
|
|
name: "projects",
|
|
component: () => import("@/views/ProjectListView.vue"),
|
|
},
|
|
{
|
|
path: "/projects/:id",
|
|
name: "project-view",
|
|
component: () => import("@/views/ProjectView.vue"),
|
|
},
|
|
{
|
|
path: "/rules",
|
|
name: "rules",
|
|
component: () => import("@/views/RulesView.vue"),
|
|
},
|
|
{
|
|
// The design systems this install RECORDS — for the projects it tracks,
|
|
// not for the install itself. There was a sibling `/design` that read the
|
|
// running app's own stylesheet out of the browser; it could only ever
|
|
// inspect the instance it was served from, which made it a mirror rather
|
|
// than a tool (#274).
|
|
path: "/design-systems",
|
|
name: "design-systems",
|
|
component: () => import("@/views/DesignSystemsView.vue"),
|
|
},
|
|
{
|
|
path: "/tasks",
|
|
redirect: "/",
|
|
},
|
|
{
|
|
path: "/tasks/new",
|
|
name: "task-new",
|
|
component: () => import("@/views/TaskEditorView.vue"),
|
|
},
|
|
{
|
|
path: "/tasks/:id",
|
|
name: "task-edit",
|
|
component: () => import("@/views/TaskEditorView.vue"),
|
|
},
|
|
{
|
|
path: "/shared",
|
|
name: "shared-with-me",
|
|
component: () => import("@/views/SharedWithMeView.vue"),
|
|
},
|
|
{
|
|
path: "/settings",
|
|
name: "settings",
|
|
component: () => import("@/views/SettingsView.vue"),
|
|
},
|
|
{
|
|
path: "/trash",
|
|
name: "trash",
|
|
component: () => import("@/views/TrashView.vue"),
|
|
},
|
|
{ path: "/admin/users", redirect: "/settings" },
|
|
{ path: "/admin/logs", redirect: "/settings" },
|
|
],
|
|
});
|
|
|
|
router.beforeEach(async (to) => {
|
|
if (to.meta.public) return;
|
|
|
|
const authStore = useAuthStore();
|
|
|
|
// Wait for initial auth check if still loading
|
|
if (authStore.loading && !authStore.isAuthenticated) {
|
|
await authStore.checkAuth();
|
|
}
|
|
|
|
if (!authStore.isAuthenticated) {
|
|
return { name: "login", query: { redirect: to.fullPath } };
|
|
}
|
|
});
|
|
|
|
export default router;
|