@donartcha/openlag 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +373 -0
- package/README.md +82 -0
- package/bin/openlag.js +2 -0
- package/dist/assets/arc-4YUHkXo3.js +1 -0
- package/dist/assets/architectureDiagram-3BPJPVTR-WeGmL7HM.js +36 -0
- package/dist/assets/blockDiagram-GPEHLZMM-CtV7ubAx.js +132 -0
- package/dist/assets/c4Diagram-AAUBKEIU-DqYDW5c3.js +10 -0
- package/dist/assets/channel-Tsel3-MK.js +1 -0
- package/dist/assets/chunk-2J33WTMH-BE8P9tjh.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-Bi7oLGF5.js +1 -0
- package/dist/assets/chunk-55IACEB6-D9Xhxp_r.js +1 -0
- package/dist/assets/chunk-727SXJPM-Dz8jKE60.js +206 -0
- package/dist/assets/chunk-AQP2D5EJ-BzmM0IeH.js +231 -0
- package/dist/assets/chunk-FMBD7UC4-Cvl5dpcx.js +15 -0
- package/dist/assets/chunk-ND2GUHAM-Dz2efqnq.js +1 -0
- package/dist/assets/chunk-QZHKN3VN-CwblgSnQ.js +1 -0
- package/dist/assets/classDiagram-4FO5ZUOK-Bgm-_cW8.js +1 -0
- package/dist/assets/classDiagram-v2-Q7XG4LA2-Bgm-_cW8.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-h_A3nZUx.js +1 -0
- package/dist/assets/cytoscape.esm-D_LviqZs.js +331 -0
- package/dist/assets/dagre-BM42HDAG-CN_B2Doz.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-2AECGRRQ-C9TAFwjG.js +43 -0
- package/dist/assets/diagram-5GNKFQAL-BThljQLo.js +10 -0
- package/dist/assets/diagram-KO2AKTUF-bRPq25Se.js +3 -0
- package/dist/assets/diagram-LMA3HP47-BubLCIus.js +24 -0
- package/dist/assets/diagram-OG6HWLK6-CJpfhIsS.js +24 -0
- package/dist/assets/erDiagram-TEJ5UH35-6Xkza9wL.js +85 -0
- package/dist/assets/flowDiagram-I6XJVG4X-Bq_to3hX.js +162 -0
- package/dist/assets/ganttDiagram-6RSMTGT7-C3CmvYl7.js +292 -0
- package/dist/assets/gitGraphDiagram-PVQCEYII-C93LTfrl.js +106 -0
- package/dist/assets/graph-CAnANduQ.js +1 -0
- package/dist/assets/index-0RMQQ34p.css +1 -0
- package/dist/assets/index-ByxguSZe.js +729 -0
- package/dist/assets/infoDiagram-5YYISTIA-CMfuwygl.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-YF4QCWOH-CbJ5ojDF.js +70 -0
- package/dist/assets/journeyDiagram-JHISSGLW-C_Xz8YyT.js +139 -0
- package/dist/assets/kanban-definition-UN3LZRKU-GVv_iRMq.js +89 -0
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-DGIYPm2g.js +1 -0
- package/dist/assets/linear-BNEtUH2J.js +1 -0
- package/dist/assets/mindmap-definition-RKZ34NQL-DIsL0XSF.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pieDiagram-4H26LBE5-CSCTSOjk.js +30 -0
- package/dist/assets/quadrantDiagram-W4KKPZXB-CQQ9OaFY.js +7 -0
- package/dist/assets/requirementDiagram-4Y6WPE33-Cjn3la_S.js +84 -0
- package/dist/assets/sankeyDiagram-5OEKKPKP-DoVspvVc.js +40 -0
- package/dist/assets/sequenceDiagram-3UESZ5HK-UsoGmL4w.js +162 -0
- package/dist/assets/stateDiagram-AJRCARHV-DLmf7Dc8.js +1 -0
- package/dist/assets/stateDiagram-v2-BHNVJYJU-jkiDZ_3u.js +1 -0
- package/dist/assets/timeline-definition-PNZ67QCA-HfyRxZ8p.js +120 -0
- package/dist/assets/vennDiagram-CIIHVFJN-B6pM3L33.js +34 -0
- package/dist/assets/wardley-L42UT6IY-B-LdKtrI.js +173 -0
- package/dist/assets/wardleyDiagram-YWT4CUSO-BD45zhOu.js +78 -0
- package/dist/assets/xychartDiagram-2RQKCTM6-zsDMbUiS.js +7 -0
- package/dist/cli/openlag.js +1793 -0
- package/dist/index.html +14 -0
- package/index.html +13 -0
- package/package.json +84 -0
- package/scripts/cli/build.ts +34 -0
- package/scripts/cli/dev.ts +35 -0
- package/scripts/cli/generate.ts +92 -0
- package/scripts/cli/init.ts +427 -0
- package/scripts/cli/lint.ts +29 -0
- package/scripts/cli/openlag.ts +110 -0
- package/scripts/cli/vite-bin.ts +8 -0
- package/scripts/core/parser/diagnostic.ts +34 -0
- package/scripts/core/parser/normalizer.ts +27 -0
- package/scripts/core/parser/scanner.ts +30 -0
- package/scripts/core/parser/schemas.ts +23 -0
- package/scripts/core/parser/types.ts +30 -0
- package/scripts/core/parser.ts +127 -0
- package/scripts/generate-relations.ts +53 -0
- package/scripts/lint/lint-engine.ts +85 -0
- package/scripts/lint/lint-profiles.ts +49 -0
- package/scripts/lint/lint-rules.ts +174 -0
- package/scripts/lint/lint-types.ts +43 -0
- package/src/App.tsx +164 -0
- package/src/components/DocumentationView.tsx +905 -0
- package/src/components/GraphView.tsx +529 -0
- package/src/components/GuideView.tsx +535 -0
- package/src/components/ImpactView.tsx +365 -0
- package/src/components/MarkdownRenderer.tsx +120 -0
- package/src/components/OrphansView.tsx +360 -0
- package/src/components/SettingsView.tsx +146 -0
- package/src/core/generated/relation-definitions.ts +622 -0
- package/src/core/graph/GraphQueryLayer.ts +194 -0
- package/src/core/registry/ArtifactRegistry.ts +19 -0
- package/src/core/registry/RelationRegistry.ts +27 -0
- package/src/core/semantic/artifact-layers.ts +43 -0
- package/src/core/semantic/ownership-rules.ts +13 -0
- package/src/core/semantic/types.ts +11 -0
- package/src/index.css +121 -0
- package/src/lib/reportUtils.ts +59 -0
- package/src/main.tsx +10 -0
- package/src/store.ts +146 -0
- package/src/types.ts +77 -0
- package/vite.config.ts +31 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { LintIssue, LintProfile } from './lint-types.js';
|
|
2
|
+
import { OpenLagData, ParsedArtifact, ParsedRelation, ParseError } from '../core/parser.js';
|
|
3
|
+
import { ArtifactRegistry } from '../../src/core/registry/ArtifactRegistry.js';
|
|
4
|
+
import { RelationRegistry } from '../../src/core/registry/RelationRegistry.js';
|
|
5
|
+
|
|
6
|
+
export function runLintRules(data: OpenLagData, profile: LintProfile): LintIssue[] {
|
|
7
|
+
const issues: LintIssue[] = [];
|
|
8
|
+
|
|
9
|
+
const getSeverity = (rule: keyof LintProfile, artifactStatus?: string) => {
|
|
10
|
+
let severity = profile[rule];
|
|
11
|
+
if (!severity || severity === 'off') return 'off';
|
|
12
|
+
|
|
13
|
+
// Downgrade severities based on drafting status
|
|
14
|
+
if (artifactStatus === 'draft') {
|
|
15
|
+
if (rule !== 'brokenRelation' && rule !== 'invalidYaml' && rule !== 'duplicateId') {
|
|
16
|
+
severity = 'info';
|
|
17
|
+
}
|
|
18
|
+
} else if (artifactStatus === 'in_progress') {
|
|
19
|
+
if (rule !== 'brokenRelation' && rule !== 'invalidYaml' && rule !== 'duplicateId') {
|
|
20
|
+
if (severity === 'error') severity = 'warning';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return severity;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const addIssue = (rule: keyof LintProfile, message: string, file?: string, artifactId?: string, artifactStatus?: string) => {
|
|
27
|
+
const severity = getSeverity(rule, artifactStatus);
|
|
28
|
+
if (severity && severity !== 'off') {
|
|
29
|
+
issues.push({ severity, rule, message, file, artifactId });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// 1. Invalid YAML
|
|
34
|
+
for (const error of data.errors) {
|
|
35
|
+
addIssue('invalidYaml', error.message, error.file);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Build ID map
|
|
39
|
+
const artifactMap = new Map<string, ParsedArtifact[]>();
|
|
40
|
+
for (const artifact of data.artifacts) {
|
|
41
|
+
const list = artifactMap.get(artifact.id) || [];
|
|
42
|
+
list.push(artifact);
|
|
43
|
+
artifactMap.set(artifact.id, list);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 2. Duplicate IDs
|
|
47
|
+
for (const [id, artifacts] of artifactMap.entries()) {
|
|
48
|
+
if (artifacts.length > 1) {
|
|
49
|
+
addIssue('duplicateId', `${id} appears in ${artifacts.length} files`, artifacts[0].file, id);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 3. Artifact type and minimal fields
|
|
54
|
+
for (const artifact of data.artifacts) {
|
|
55
|
+
if (!ArtifactRegistry.isValid(artifact.type)) {
|
|
56
|
+
addIssue('invalidArtifactType', `Invalid artifact type: ${artifact.type}`, artifact.file, artifact.id, artifact.status);
|
|
57
|
+
}
|
|
58
|
+
if (!artifact.type || !artifact.title) {
|
|
59
|
+
addIssue('missingRequiredFields', `Artifact missing type or title`, artifact.file, artifact.id, artifact.status);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Compute relation maps
|
|
64
|
+
const targets = new Set(data.artifacts.map(a => a.id));
|
|
65
|
+
const implementationsByReq = new Map<string, ParsedRelation[]>();
|
|
66
|
+
const testsByReq = new Map<string, ParsedRelation[]>();
|
|
67
|
+
const reqsByCode = new Map<string, ParsedRelation[]>();
|
|
68
|
+
const reqsByTest = new Map<string, ParsedRelation[]>();
|
|
69
|
+
|
|
70
|
+
for (const relation of data.relations) {
|
|
71
|
+
if (!RelationRegistry.isValid(relation.type)) {
|
|
72
|
+
addIssue('invalidRelationType', `Invalid relation type: ${relation.type}`, relation.file, relation.from);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 4. Broken relations
|
|
76
|
+
if (!targets.has(relation.to)) {
|
|
77
|
+
addIssue('brokenRelation', `${relation.from} -> ${relation.to} target does not exist`, relation.file, relation.from);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Populate relation graphs for rules
|
|
81
|
+
if (relation.type === 'IMPLEMENTS') {
|
|
82
|
+
const list = implementationsByReq.get(relation.to) || [];
|
|
83
|
+
list.push(relation);
|
|
84
|
+
implementationsByReq.set(relation.to, list);
|
|
85
|
+
|
|
86
|
+
const listCode = reqsByCode.get(relation.from) || [];
|
|
87
|
+
listCode.push(relation);
|
|
88
|
+
reqsByCode.set(relation.from, listCode);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (relation.type === 'TESTS' || relation.type === 'VALIDATES') {
|
|
92
|
+
const list = testsByReq.get(relation.to) || [];
|
|
93
|
+
list.push(relation);
|
|
94
|
+
testsByReq.set(relation.to, list);
|
|
95
|
+
|
|
96
|
+
const listTest = reqsByTest.get(relation.from) || [];
|
|
97
|
+
listTest.push(relation);
|
|
98
|
+
reqsByTest.set(relation.from, listTest);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Rule functions per artifact
|
|
103
|
+
for (const artifact of data.artifacts) {
|
|
104
|
+
const isDraft = artifact.status === 'draft';
|
|
105
|
+
const isClosed = artifact.status === 'closed';
|
|
106
|
+
const isDeprecated = artifact.status === 'deprecated';
|
|
107
|
+
|
|
108
|
+
if (isDeprecated) continue; // Skip rules for deprecated except broken relations (already checked above)
|
|
109
|
+
|
|
110
|
+
if (artifact.type === 'REQUIREMENT') {
|
|
111
|
+
const hasImplementation = implementationsByReq.has(artifact.id);
|
|
112
|
+
const hasTest = testsByReq.has(artifact.id);
|
|
113
|
+
|
|
114
|
+
if (!hasImplementation) {
|
|
115
|
+
addIssue('requirementWithoutImplementation', `${artifact.id} lacks implementation`, artifact.file, artifact.id, artifact.status);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!hasTest) {
|
|
119
|
+
addIssue('requirementWithoutTest', `${artifact.id} has no tests linked`, artifact.file, artifact.id, artifact.status);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (artifact.type === 'CODE_ENTITY') {
|
|
124
|
+
const hasReq = reqsByCode.has(artifact.id);
|
|
125
|
+
if (!hasReq) {
|
|
126
|
+
addIssue('codeWithoutRequirement', `${artifact.id} has no requirement associated`, artifact.file, artifact.id, artifact.status);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (artifact.type === 'TEST') {
|
|
131
|
+
const hasReq = reqsByTest.has(artifact.id);
|
|
132
|
+
if (!hasReq) {
|
|
133
|
+
addIssue('orphanArtifact', `${artifact.id} is a test without associated requirement`, artifact.file, artifact.id, artifact.status);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (isClosed) {
|
|
138
|
+
const outgoing = data.relations.filter(r => r.from === artifact.id);
|
|
139
|
+
for (const rel of outgoing) {
|
|
140
|
+
const targetArts = artifactMap.get(rel.to);
|
|
141
|
+
if (targetArts && targetArts[0]) {
|
|
142
|
+
const targetStatus = targetArts[0].status;
|
|
143
|
+
if (targetStatus === 'draft' || targetStatus === 'in_progress') {
|
|
144
|
+
// Check relation rules: RELATES_TO, DOCUMENTS, JUSTIFIES don't break closed state
|
|
145
|
+
if (rel.type !== 'RELATES_TO' && rel.type !== 'DOCUMENTS' && rel.type !== 'JUSTIFIES') {
|
|
146
|
+
addIssue('closedArtifactWithPendingRelations', `${artifact.id} is closed but links to ${targetStatus} artifact ${rel.to} via ${rel.type}`, artifact.file, artifact.id, artifact.status);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!artifact.ownership || Object.keys(artifact.ownership).length === 0) {
|
|
153
|
+
addIssue('missingOwnership', `${artifact.id} is closed but has no ownership defined`, artifact.file, artifact.id, artifact.status);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (artifact.type === 'API' && (!artifact.ownership || Object.keys(artifact.ownership).length === 0)) {
|
|
158
|
+
addIssue('missingOwnership', `API ${artifact.id} should have ownership defined`, artifact.file, artifact.id, artifact.status);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check layer semantics
|
|
162
|
+
if (artifact.layer === 'BUSINESS') {
|
|
163
|
+
const outgoing = data.relations.filter(r => r.from === artifact.id);
|
|
164
|
+
for (const rel of outgoing) {
|
|
165
|
+
const relSemantics = rel.category;
|
|
166
|
+
if (relSemantics === 'OPERATIONAL') {
|
|
167
|
+
addIssue('invalidLayerRelation', `Business layer artifact ${artifact.id} should not have OPERATIONAL relations (${rel.type})`, artifact.file, artifact.id, artifact.status);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return issues;
|
|
174
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type LintSeverity = 'error' | 'warning' | 'info' | 'off';
|
|
2
|
+
|
|
3
|
+
export interface LintIssue {
|
|
4
|
+
severity: LintSeverity;
|
|
5
|
+
rule: string;
|
|
6
|
+
artifactId?: string;
|
|
7
|
+
message: string;
|
|
8
|
+
file?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface LintSummary {
|
|
12
|
+
errors: number;
|
|
13
|
+
warnings: number;
|
|
14
|
+
info: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LintReport {
|
|
18
|
+
profile: string;
|
|
19
|
+
summary: LintSummary;
|
|
20
|
+
issues: LintIssue[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface LintProfile {
|
|
24
|
+
duplicateId: LintSeverity;
|
|
25
|
+
invalidYaml: LintSeverity;
|
|
26
|
+
brokenRelation: LintSeverity;
|
|
27
|
+
missingRequiredFields: LintSeverity;
|
|
28
|
+
requirementWithoutImplementation: LintSeverity;
|
|
29
|
+
requirementWithoutTest: LintSeverity;
|
|
30
|
+
codeWithoutRequirement: LintSeverity;
|
|
31
|
+
closedArtifactWithPendingRelations: LintSeverity;
|
|
32
|
+
orphanArtifact: LintSeverity;
|
|
33
|
+
invalidRelationType: LintSeverity;
|
|
34
|
+
invalidArtifactType: LintSeverity;
|
|
35
|
+
invalidLayerRelation?: LintSeverity;
|
|
36
|
+
missingOwnership?: LintSeverity;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface LintConfig {
|
|
40
|
+
defaultProfile: string;
|
|
41
|
+
failOnWarnings: boolean;
|
|
42
|
+
profiles: Record<string, Partial<LintProfile>>;
|
|
43
|
+
}
|
package/src/App.tsx
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { useStore } from './store';
|
|
3
|
+
import { GraphView } from './components/GraphView';
|
|
4
|
+
import { DocumentationView } from './components/DocumentationView';
|
|
5
|
+
import { ImpactView } from './components/ImpactView';
|
|
6
|
+
import { OrphansView } from './components/OrphansView';
|
|
7
|
+
import { GuideView } from './components/GuideView';
|
|
8
|
+
import { SettingsView } from './components/SettingsView';
|
|
9
|
+
import { Network, FileText, GitPullRequest, Settings, Database, AlertCircle, BookOpen } from 'lucide-react';
|
|
10
|
+
|
|
11
|
+
export default function App() {
|
|
12
|
+
const {
|
|
13
|
+
initializeStore,
|
|
14
|
+
versions,
|
|
15
|
+
currentVersionId,
|
|
16
|
+
setVersion,
|
|
17
|
+
activeView,
|
|
18
|
+
setView,
|
|
19
|
+
isLoading,
|
|
20
|
+
systemVersions
|
|
21
|
+
} = useStore();
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
initializeStore();
|
|
25
|
+
}, [initializeStore]);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="flex h-screen w-screen overflow-hidden bg-[#0a0a0a] text-[#e0e0e0] font-sans selection:bg-emerald-500/30 print-block">
|
|
29
|
+
{/* Sidebar Nav */}
|
|
30
|
+
<nav className="w-16 flex flex-col items-center py-6 bg-[#0c0c0c] text-white/40 border-r border-white/10 shrink-0">
|
|
31
|
+
<div className="w-8 h-8 bg-white flex items-center justify-center rounded-sm mb-8">
|
|
32
|
+
<div className="w-4 h-4 border-2 border-black rotate-45"></div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div className="flex flex-col gap-6 w-full items-center">
|
|
36
|
+
<button
|
|
37
|
+
onClick={() => setView('graph')}
|
|
38
|
+
className={`transition-all hover:text-white hover:opacity-100 ${activeView === 'graph' ? 'text-white opacity-100' : 'opacity-40'}`}
|
|
39
|
+
title="Graph View"
|
|
40
|
+
>
|
|
41
|
+
<Network size={20} strokeWidth={1.5} />
|
|
42
|
+
</button>
|
|
43
|
+
<button
|
|
44
|
+
onClick={() => setView('docs')}
|
|
45
|
+
className={`transition-all hover:text-white hover:opacity-100 ${activeView === 'docs' ? 'text-white opacity-100' : 'opacity-40'}`}
|
|
46
|
+
title="Documentation Engine"
|
|
47
|
+
>
|
|
48
|
+
<FileText size={20} strokeWidth={1.5} />
|
|
49
|
+
</button>
|
|
50
|
+
<button
|
|
51
|
+
onClick={() => setView('impact')}
|
|
52
|
+
className={`transition-all hover:text-white hover:opacity-100 ${activeView === 'impact' ? 'text-white opacity-100' : 'opacity-40'}`}
|
|
53
|
+
title="Impact Analysis"
|
|
54
|
+
>
|
|
55
|
+
<GitPullRequest size={20} strokeWidth={1.5} />
|
|
56
|
+
</button>
|
|
57
|
+
<button
|
|
58
|
+
onClick={() => setView('orphans')}
|
|
59
|
+
className={`transition-all hover:text-white hover:opacity-100 ${activeView === 'orphans' ? 'text-red-400 opacity-100' : 'opacity-40'}`}
|
|
60
|
+
title="Traceability GAPs"
|
|
61
|
+
>
|
|
62
|
+
<AlertCircle size={20} strokeWidth={1.5} />
|
|
63
|
+
</button>
|
|
64
|
+
<button
|
|
65
|
+
onClick={() => setView('guide')}
|
|
66
|
+
className={`transition-all hover:text-white hover:opacity-100 ${activeView === 'guide' ? 'text-amber-400 opacity-100' : 'opacity-40'}`}
|
|
67
|
+
title="Usage Guide"
|
|
68
|
+
>
|
|
69
|
+
<BookOpen size={20} strokeWidth={1.5} />
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div className="mt-auto">
|
|
74
|
+
<button
|
|
75
|
+
onClick={() => setView('settings')}
|
|
76
|
+
className={`transition-all hover:text-white hover:opacity-100 ${activeView === 'settings' ? 'text-white opacity-100' : 'opacity-40'}`}
|
|
77
|
+
title="Settings"
|
|
78
|
+
>
|
|
79
|
+
<Settings size={20} strokeWidth={1.5} />
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
</nav>
|
|
83
|
+
|
|
84
|
+
{/* Main Content Area */}
|
|
85
|
+
<div className="flex-1 flex flex-col min-w-0">
|
|
86
|
+
|
|
87
|
+
{/* Top Header */}
|
|
88
|
+
<header className="h-16 bg-[#0f0f0f] border-b border-white/10 flex items-center px-8 justify-between shrink-0 z-10">
|
|
89
|
+
<div className="font-serif text-xl italic tracking-tight flex items-center gap-2">
|
|
90
|
+
OpenLAG <span className="text-xs font-mono opacity-50 ml-2 not-italic">| Lifecycle Engine</span>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div className="flex items-center gap-6">
|
|
94
|
+
<div className="flex items-center gap-3">
|
|
95
|
+
<div className="flex flex-col items-end">
|
|
96
|
+
<span className="text-[10px] opacity-40 uppercase tracking-widest">Doc Snapshot</span>
|
|
97
|
+
</div>
|
|
98
|
+
<select
|
|
99
|
+
value={currentVersionId || ''}
|
|
100
|
+
onChange={(e) => setVersion(e.target.value)}
|
|
101
|
+
className="bg-[#0c0c0c] border border-white/20 text-xs font-mono text-emerald-400 rounded-sm px-3 py-1.5 outline-none cursor-pointer hover:bg-white/5 transition-colors focus:border-emerald-400"
|
|
102
|
+
>
|
|
103
|
+
{versions.map(v => (
|
|
104
|
+
<option key={v.id} value={v.id} className="bg-[#0c0c0c] text-white">
|
|
105
|
+
{v.name} ({new Date(v.timestamp).toLocaleDateString()})
|
|
106
|
+
</option>
|
|
107
|
+
))}
|
|
108
|
+
</select>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div className="h-6 w-[1px] bg-white/10" />
|
|
112
|
+
|
|
113
|
+
<div className="flex items-center gap-3 group relative">
|
|
114
|
+
<div className="p-2 bg-emerald-500/10 rounded-full border border-emerald-500/20">
|
|
115
|
+
<Database size={14} className="text-emerald-400" />
|
|
116
|
+
</div>
|
|
117
|
+
<div className="flex flex-col">
|
|
118
|
+
<span className="text-[10px] opacity-40 uppercase tracking-widest leading-none">System State</span>
|
|
119
|
+
<div className="flex items-center gap-2 mt-1">
|
|
120
|
+
<span className="text-[10px] text-white/80 font-bold tracking-tight">
|
|
121
|
+
{systemVersions.length} Components Active
|
|
122
|
+
</span>
|
|
123
|
+
<div className="w-1.5 h-1.5 bg-emerald-500 rounded-full animate-pulse shadow-[0_0_8px_rgba(16,185,129,0.5)]" />
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{/* Tooltip on hover */}
|
|
128
|
+
<div className="absolute top-full right-0 mt-2 w-64 bg-[#0f0f0f] border border-white/10 p-4 rounded-sm shadow-2xl opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50">
|
|
129
|
+
<div className="text-[9px] uppercase tracking-widest text-[#888] font-bold mb-3 border-b border-white/5 pb-2">Active Inventory</div>
|
|
130
|
+
<div className="space-y-2">
|
|
131
|
+
{systemVersions.map(sv => (
|
|
132
|
+
<div key={sv.id} className="flex justify-between items-center text-[10px]">
|
|
133
|
+
<span className="text-white/60">{sv.component}</span>
|
|
134
|
+
<span className="font-mono text-emerald-500/60">{sv.version}</span>
|
|
135
|
+
</div>
|
|
136
|
+
))}
|
|
137
|
+
{systemVersions.length === 0 && (
|
|
138
|
+
<div className="text-white/20 italic">No system components logged.</div>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</header>
|
|
145
|
+
|
|
146
|
+
{/* View Renderer */}
|
|
147
|
+
<main className="flex-1 relative overflow-hidden">
|
|
148
|
+
{isLoading && !currentVersionId ? (
|
|
149
|
+
<div className="absolute inset-0 flex items-center justify-center bg-white/50 backdrop-blur-sm z-50">
|
|
150
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
|
151
|
+
</div>
|
|
152
|
+
) : null}
|
|
153
|
+
|
|
154
|
+
{activeView === 'graph' && <GraphView />}
|
|
155
|
+
{activeView === 'docs' && <DocumentationView />}
|
|
156
|
+
{activeView === 'impact' && <ImpactView />}
|
|
157
|
+
{activeView === 'orphans' && <OrphansView />}
|
|
158
|
+
{activeView === 'guide' && <GuideView />}
|
|
159
|
+
{activeView === 'settings' && <SettingsView />}
|
|
160
|
+
</main>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|