strategic-review-webui 1.0.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/README.md +36 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +27 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/reportService.d.ts +18 -0
- package/dist/reportService.d.ts.map +1 -0
- package/dist/reportService.js +170 -0
- package/dist/reportService.js.map +1 -0
- package/dist/routes.d.ts +2 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +119 -0
- package/dist/routes.js.map +1 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +38 -0
- package/public/style.css +107 -0
- package/views/detail.ejs +127 -0
- package/views/error.ejs +9 -0
- package/views/layouts/foot.ejs +3 -0
- package/views/layouts/head.ejs +15 -0
- package/views/list.ejs +46 -0
- package/views/new.ejs +33 -0
- package/views/partials/badge.ejs +4 -0
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# strategic-review-webui
|
|
2
|
+
|
|
3
|
+
Web UI server for reviewing strategic reports stored in `.strategic/` directory.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g strategic-review-webui
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Run the server in any directory that contains a `.strategic/` folder:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
strategic-review-webui
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or with a custom port:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
strategic-review-webui --port 8080
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then open `http://localhost:3000` in your browser.
|
|
26
|
+
|
|
27
|
+
## npx (no installation)
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx strategic-review-webui
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Requirements
|
|
34
|
+
|
|
35
|
+
- Node.js >= 18
|
|
36
|
+
- `.strategic/` directory with `.md` report files
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
// Parse --port argument
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const portIdx = args.indexOf('--port');
|
|
12
|
+
if (portIdx !== -1 && args[portIdx + 1]) {
|
|
13
|
+
process.env.PORT = args[portIdx + 1];
|
|
14
|
+
}
|
|
15
|
+
// Set .strategic directory based on current working directory
|
|
16
|
+
const strategicDir = path_1.default.join(process.cwd(), '.strategic');
|
|
17
|
+
process.env.STRATEGIC_DIR = strategicDir;
|
|
18
|
+
// Friendly message when .strategic directory doesn't exist
|
|
19
|
+
if (!fs_1.default.existsSync(strategicDir)) {
|
|
20
|
+
console.log(`[strategic-review-webui] .strategic/ directory not found in ${process.cwd()}`);
|
|
21
|
+
console.log(`[strategic-review-webui] Creating .strategic/ directory...`);
|
|
22
|
+
fs_1.default.mkdirSync(strategicDir, { recursive: true });
|
|
23
|
+
console.log(`[strategic-review-webui] Done. Place your .md report files in ${strategicDir}`);
|
|
24
|
+
}
|
|
25
|
+
// Start the server
|
|
26
|
+
require('./index');
|
|
27
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";;;;;;AACA,4CAAmB;AACnB,gDAAuB;AAEvB,wBAAwB;AACxB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AAClC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;AACtC,IAAI,OAAO,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,EAAE,CAAC;IACxC,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAA;AACtC,CAAC;AAED,8DAA8D;AAC9D,MAAM,YAAY,GAAG,cAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,YAAY,CAAC,CAAA;AAC3D,OAAO,CAAC,GAAG,CAAC,aAAa,GAAG,YAAY,CAAA;AAExC,2DAA2D;AAC3D,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;IACjC,OAAO,CAAC,GAAG,CAAC,+DAA+D,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;IAC3F,OAAO,CAAC,GAAG,CAAC,4DAA4D,CAAC,CAAA;IACzE,YAAE,CAAC,SAAS,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAC/C,OAAO,CAAC,GAAG,CAAC,iEAAiE,YAAY,EAAE,CAAC,CAAA;AAC9F,CAAC;AAED,mBAAmB;AACnB,OAAO,CAAC,SAAS,CAAC,CAAA"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const express_1 = __importDefault(require("express"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const routes_1 = require("./routes");
|
|
9
|
+
const app = (0, express_1.default)();
|
|
10
|
+
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
|
|
11
|
+
app.set('view engine', 'ejs');
|
|
12
|
+
app.set('views', path_1.default.join(__dirname, '..', 'views'));
|
|
13
|
+
app.use(express_1.default.urlencoded({ extended: false }));
|
|
14
|
+
app.use(express_1.default.json());
|
|
15
|
+
app.use(express_1.default.static(path_1.default.join(__dirname, '..', 'public')));
|
|
16
|
+
app.use('/', routes_1.router);
|
|
17
|
+
app.listen(PORT, () => {
|
|
18
|
+
console.log(`Strategic Review Server running at http://localhost:${PORT}`);
|
|
19
|
+
});
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;AAAA,sDAA6B;AAC7B,gDAAuB;AACvB,qCAAiC;AAEjC,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAA;AACrB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AAErE,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,KAAK,CAAC,CAAA;AAC7B,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAA;AAErD,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,UAAU,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;AAChD,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,IAAI,EAAE,CAAC,CAAA;AACvB,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,MAAM,CAAC,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAA;AAC7D,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,eAAM,CAAC,CAAA;AAEpB,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;IACpB,OAAO,CAAC,GAAG,CAAC,uDAAuD,IAAI,EAAE,CAAC,CAAA;AAC5E,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ReportFile, ReportGroup, PaginatedGroups } from './types';
|
|
2
|
+
export declare function ensureStrategicDir(): void;
|
|
3
|
+
export declare function isValidFilename(filename: string): boolean;
|
|
4
|
+
export declare function parseFilename(filename: string): {
|
|
5
|
+
prefix: string;
|
|
6
|
+
version: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function getPaginatedGroups(page: number): PaginatedGroups;
|
|
9
|
+
export declare function getReportByFilename(filename: string): ReportFile;
|
|
10
|
+
export declare function getGroupByPrefix(prefix: string): ReportGroup;
|
|
11
|
+
export declare function createReport(params: {
|
|
12
|
+
name: string;
|
|
13
|
+
objective: string;
|
|
14
|
+
constraints: string;
|
|
15
|
+
}): string;
|
|
16
|
+
export declare function approveReport(filename: string, comment?: string): void;
|
|
17
|
+
export declare function rejectReport(filename: string, comment: string): string;
|
|
18
|
+
//# sourceMappingURL=reportService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reportService.d.ts","sourceRoot":"","sources":["../src/reportService.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAgB,eAAe,EAAE,MAAM,SAAS,CAAA;AAMhF,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAOnF;AAuBD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAkChE;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAGhE;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,CAkB5D;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;CACpB,GAAG,MAAM,CA6BT;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAgBtE;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA0BtE"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ensureStrategicDir = ensureStrategicDir;
|
|
7
|
+
exports.isValidFilename = isValidFilename;
|
|
8
|
+
exports.parseFilename = parseFilename;
|
|
9
|
+
exports.getPaginatedGroups = getPaginatedGroups;
|
|
10
|
+
exports.getReportByFilename = getReportByFilename;
|
|
11
|
+
exports.getGroupByPrefix = getGroupByPrefix;
|
|
12
|
+
exports.createReport = createReport;
|
|
13
|
+
exports.approveReport = approveReport;
|
|
14
|
+
exports.rejectReport = rejectReport;
|
|
15
|
+
const fs_1 = __importDefault(require("fs"));
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
17
|
+
const gray_matter_1 = __importDefault(require("gray-matter"));
|
|
18
|
+
const STRATEGIC_DIR = process.env.STRATEGIC_DIR || path_1.default.join(process.cwd(), '.strategic');
|
|
19
|
+
const FILENAME_REGEX = /^\d{8}_\d{6}\.v\d+\.md$/;
|
|
20
|
+
const PAGE_SIZE = 10;
|
|
21
|
+
function ensureStrategicDir() {
|
|
22
|
+
fs_1.default.mkdirSync(STRATEGIC_DIR, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
function isValidFilename(filename) {
|
|
25
|
+
return FILENAME_REGEX.test(filename);
|
|
26
|
+
}
|
|
27
|
+
function parseFilename(filename) {
|
|
28
|
+
const match = filename.match(/^(\d{8}_\d{6})\.v(\d+)\.md$/);
|
|
29
|
+
if (!match)
|
|
30
|
+
throw new Error(`Invalid filename: ${filename}`);
|
|
31
|
+
return {
|
|
32
|
+
prefix: match[1],
|
|
33
|
+
version: parseInt(match[2], 10),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function parseReportFile(filename) {
|
|
37
|
+
const filePath = path_1.default.join(STRATEGIC_DIR, filename);
|
|
38
|
+
const raw = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
39
|
+
const parsed = (0, gray_matter_1.default)(raw);
|
|
40
|
+
const { prefix, version } = parseFilename(filename);
|
|
41
|
+
return {
|
|
42
|
+
filename,
|
|
43
|
+
prefix,
|
|
44
|
+
version,
|
|
45
|
+
name: String(parsed.data.name ?? ''),
|
|
46
|
+
objective: String(parsed.data.objective ?? ''),
|
|
47
|
+
constraints: String(parsed.data.constraints ?? ''),
|
|
48
|
+
status: (parsed.data.status ?? 'init'),
|
|
49
|
+
reviewComment: parsed.data['review-comment']
|
|
50
|
+
? String(parsed.data['review-comment'])
|
|
51
|
+
: undefined,
|
|
52
|
+
content: parsed.content.trim(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function getPaginatedGroups(page) {
|
|
56
|
+
ensureStrategicDir();
|
|
57
|
+
const allFiles = fs_1.default.readdirSync(STRATEGIC_DIR).filter(isValidFilename);
|
|
58
|
+
const prefixMap = new Map();
|
|
59
|
+
for (const filename of allFiles) {
|
|
60
|
+
const { prefix } = parseFilename(filename);
|
|
61
|
+
if (!prefixMap.has(prefix))
|
|
62
|
+
prefixMap.set(prefix, []);
|
|
63
|
+
prefixMap.get(prefix).push(filename);
|
|
64
|
+
}
|
|
65
|
+
const sortedPrefixes = Array.from(prefixMap.keys()).sort((a, b) => b.localeCompare(a));
|
|
66
|
+
const totalCount = sortedPrefixes.length;
|
|
67
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
|
|
68
|
+
const safePage = Math.min(Math.max(1, page), totalPages);
|
|
69
|
+
const startIdx = (safePage - 1) * PAGE_SIZE;
|
|
70
|
+
const slicedPrefixes = sortedPrefixes.slice(startIdx, startIdx + PAGE_SIZE);
|
|
71
|
+
const groups = slicedPrefixes.map((prefix) => {
|
|
72
|
+
const filenames = prefixMap.get(prefix).sort((a, b) => {
|
|
73
|
+
const va = parseFilename(a).version;
|
|
74
|
+
const vb = parseFilename(b).version;
|
|
75
|
+
return vb - va;
|
|
76
|
+
});
|
|
77
|
+
const allFilesParsed = filenames.map(parseReportFile);
|
|
78
|
+
return {
|
|
79
|
+
prefix,
|
|
80
|
+
latestFile: allFilesParsed[0],
|
|
81
|
+
allFiles: allFilesParsed,
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
return { groups, currentPage: safePage, totalPages, totalCount };
|
|
85
|
+
}
|
|
86
|
+
function getReportByFilename(filename) {
|
|
87
|
+
if (!isValidFilename(filename))
|
|
88
|
+
throw new Error('Invalid filename');
|
|
89
|
+
return parseReportFile(filename);
|
|
90
|
+
}
|
|
91
|
+
function getGroupByPrefix(prefix) {
|
|
92
|
+
ensureStrategicDir();
|
|
93
|
+
const allFiles = fs_1.default.readdirSync(STRATEGIC_DIR).filter(isValidFilename);
|
|
94
|
+
const filenames = allFiles
|
|
95
|
+
.filter((f) => f.startsWith(prefix))
|
|
96
|
+
.sort((a, b) => parseFilename(b).version - parseFilename(a).version);
|
|
97
|
+
if (filenames.length === 0) {
|
|
98
|
+
throw new Error(`No files found for prefix: ${prefix}`);
|
|
99
|
+
}
|
|
100
|
+
const allFilesParsed = filenames.map(parseReportFile);
|
|
101
|
+
return {
|
|
102
|
+
prefix,
|
|
103
|
+
latestFile: allFilesParsed[0],
|
|
104
|
+
allFiles: allFilesParsed,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function createReport(params) {
|
|
108
|
+
ensureStrategicDir();
|
|
109
|
+
const now = new Date();
|
|
110
|
+
const pad = (n, len = 2) => String(n).padStart(len, '0');
|
|
111
|
+
const prefix = [
|
|
112
|
+
now.getFullYear(),
|
|
113
|
+
pad(now.getMonth() + 1),
|
|
114
|
+
pad(now.getDate()),
|
|
115
|
+
'_',
|
|
116
|
+
pad(now.getHours()),
|
|
117
|
+
pad(now.getMinutes()),
|
|
118
|
+
pad(now.getSeconds()),
|
|
119
|
+
].join('');
|
|
120
|
+
const filename = `${prefix}.v1.md`;
|
|
121
|
+
const filePath = path_1.default.join(STRATEGIC_DIR, filename);
|
|
122
|
+
const frontmatter = {
|
|
123
|
+
name: params.name,
|
|
124
|
+
objective: params.objective,
|
|
125
|
+
constraints: params.constraints,
|
|
126
|
+
status: 'init',
|
|
127
|
+
};
|
|
128
|
+
const fileContent = gray_matter_1.default.stringify('', frontmatter);
|
|
129
|
+
fs_1.default.writeFileSync(filePath, fileContent, 'utf-8');
|
|
130
|
+
return filename;
|
|
131
|
+
}
|
|
132
|
+
function approveReport(filename, comment) {
|
|
133
|
+
if (!isValidFilename(filename))
|
|
134
|
+
throw new Error('Invalid filename');
|
|
135
|
+
const filePath = path_1.default.join(STRATEGIC_DIR, filename);
|
|
136
|
+
const raw = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
137
|
+
const parsed = (0, gray_matter_1.default)(raw);
|
|
138
|
+
parsed.data.status = 'approve';
|
|
139
|
+
if (comment) {
|
|
140
|
+
parsed.data['review-comment'] = comment;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
delete parsed.data['review-comment'];
|
|
144
|
+
}
|
|
145
|
+
const updated = gray_matter_1.default.stringify(parsed.content, parsed.data);
|
|
146
|
+
fs_1.default.writeFileSync(filePath, updated, 'utf-8');
|
|
147
|
+
}
|
|
148
|
+
function rejectReport(filename, comment) {
|
|
149
|
+
if (!isValidFilename(filename))
|
|
150
|
+
throw new Error('Invalid filename');
|
|
151
|
+
const filePath = path_1.default.join(STRATEGIC_DIR, filename);
|
|
152
|
+
const raw = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
153
|
+
const parsed = (0, gray_matter_1.default)(raw);
|
|
154
|
+
parsed.data.status = 'reject';
|
|
155
|
+
parsed.data['review-comment'] = comment;
|
|
156
|
+
const updated = gray_matter_1.default.stringify(parsed.content, parsed.data);
|
|
157
|
+
fs_1.default.writeFileSync(filePath, updated, 'utf-8');
|
|
158
|
+
const { prefix, version } = parseFilename(filename);
|
|
159
|
+
const newFilename = `${prefix}.v${version + 1}.md`;
|
|
160
|
+
const newFilePath = path_1.default.join(STRATEGIC_DIR, newFilename);
|
|
161
|
+
const newFrontmatter = {
|
|
162
|
+
name: String(parsed.data.name ?? ''),
|
|
163
|
+
objective: String(parsed.data.objective ?? ''),
|
|
164
|
+
constraints: String(parsed.data.constraints ?? ''),
|
|
165
|
+
status: 'revision',
|
|
166
|
+
};
|
|
167
|
+
fs_1.default.writeFileSync(newFilePath, gray_matter_1.default.stringify('', newFrontmatter), 'utf-8');
|
|
168
|
+
return newFilename;
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=reportService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reportService.js","sourceRoot":"","sources":["../src/reportService.ts"],"names":[],"mappings":"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"}
|
package/dist/routes.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,MAAM,4CAAW,CAAA"}
|
package/dist/routes.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.router = void 0;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
const marked_1 = require("marked");
|
|
6
|
+
const reportService_1 = require("./reportService");
|
|
7
|
+
exports.router = (0, express_1.Router)();
|
|
8
|
+
function handleError(res, err) {
|
|
9
|
+
if (err instanceof Error) {
|
|
10
|
+
if (err.message === 'Invalid filename' || err.message.startsWith('ENOENT')) {
|
|
11
|
+
res.status(404).render('error', { message: 'Report not found.', code: 404 });
|
|
12
|
+
}
|
|
13
|
+
else if (err.code === 'ENOENT') {
|
|
14
|
+
res.status(404).render('error', { message: 'Report not found.', code: 404 });
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
console.error(err);
|
|
18
|
+
res.status(500).render('error', { message: 'Internal server error.', code: 500 });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
console.error(err);
|
|
23
|
+
res.status(500).render('error', { message: 'Internal server error.', code: 500 });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// GET / — 목록 (10건/페이지, 최신순)
|
|
27
|
+
exports.router.get('/', (req, res) => {
|
|
28
|
+
try {
|
|
29
|
+
const page = parseInt(String(req.query.page ?? '1'), 10) || 1;
|
|
30
|
+
const data = (0, reportService_1.getPaginatedGroups)(page);
|
|
31
|
+
res.render('list', data);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
handleError(res, err);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
// GET /reports/new — 등록 폼
|
|
38
|
+
exports.router.get('/reports/new', (_req, res) => {
|
|
39
|
+
res.render('new', { error: undefined });
|
|
40
|
+
});
|
|
41
|
+
// POST /reports — 보고서 생성
|
|
42
|
+
exports.router.post('/reports', (req, res) => {
|
|
43
|
+
try {
|
|
44
|
+
const name = String(req.body.name ?? '').trim();
|
|
45
|
+
const objective = String(req.body.objective ?? '').trim();
|
|
46
|
+
const constraints = String(req.body.constraints ?? '').trim();
|
|
47
|
+
if (!name) {
|
|
48
|
+
res.render('new', { error: 'Report name is required.' });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!objective) {
|
|
52
|
+
res.render('new', { error: 'Objective is required.' });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const filename = (0, reportService_1.createReport)({ name, objective, constraints });
|
|
56
|
+
res.redirect(`/reports/${filename}`);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
handleError(res, err);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
// GET /reports/:filename — 상세
|
|
63
|
+
exports.router.get('/reports/:filename', async (req, res) => {
|
|
64
|
+
try {
|
|
65
|
+
const { filename } = req.params;
|
|
66
|
+
const file = (0, reportService_1.getReportByFilename)(filename);
|
|
67
|
+
const group = (0, reportService_1.getGroupByPrefix)(file.prefix);
|
|
68
|
+
const renderedContent = file.content
|
|
69
|
+
? String(await marked_1.marked.parse(file.content))
|
|
70
|
+
: '<p style="color:#888">No content available.</p>';
|
|
71
|
+
res.render('detail', { file, group, renderedContent });
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
handleError(res, err);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
// POST /reports/:filename/approve — 승인
|
|
78
|
+
exports.router.post('/reports/:filename/approve', async (req, res) => {
|
|
79
|
+
try {
|
|
80
|
+
const { filename } = req.params;
|
|
81
|
+
const comment = String(req.body.comment ?? '').trim();
|
|
82
|
+
if (!comment) {
|
|
83
|
+
const file = (0, reportService_1.getReportByFilename)(filename);
|
|
84
|
+
const group = (0, reportService_1.getGroupByPrefix)(file.prefix);
|
|
85
|
+
const renderedContent = file.content
|
|
86
|
+
? String(await marked_1.marked.parse(file.content))
|
|
87
|
+
: '<p style="color:#888">No content available.</p>';
|
|
88
|
+
res.render('detail', { file, group, renderedContent });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
(0, reportService_1.approveReport)(filename, comment);
|
|
92
|
+
res.redirect(`/reports/${filename}`);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
handleError(res, err);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
// POST /reports/:filename/reject — 반려
|
|
99
|
+
exports.router.post('/reports/:filename/reject', async (req, res) => {
|
|
100
|
+
try {
|
|
101
|
+
const { filename } = req.params;
|
|
102
|
+
const comment = String(req.body.comment ?? '').trim();
|
|
103
|
+
if (!comment) {
|
|
104
|
+
const file = (0, reportService_1.getReportByFilename)(filename);
|
|
105
|
+
const group = (0, reportService_1.getGroupByPrefix)(file.prefix);
|
|
106
|
+
const renderedContent = file.content
|
|
107
|
+
? String(await marked_1.marked.parse(file.content))
|
|
108
|
+
: '<p style="color:#888">No content available.</p>';
|
|
109
|
+
res.render('detail', { file, group, renderedContent });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const newFilename = (0, reportService_1.rejectReport)(filename, comment);
|
|
113
|
+
res.redirect(`/reports/${newFilename}`);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
handleError(res, err);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type ReportStatus = 'init' | 'submit' | 'approve' | 'reject' | 'revision';
|
|
2
|
+
export interface ReportFile {
|
|
3
|
+
filename: string;
|
|
4
|
+
prefix: string;
|
|
5
|
+
version: number;
|
|
6
|
+
name: string;
|
|
7
|
+
objective: string;
|
|
8
|
+
constraints: string;
|
|
9
|
+
status: ReportStatus;
|
|
10
|
+
reviewComment?: string;
|
|
11
|
+
content: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ReportGroup {
|
|
14
|
+
prefix: string;
|
|
15
|
+
latestFile: ReportFile;
|
|
16
|
+
allFiles: ReportFile[];
|
|
17
|
+
}
|
|
18
|
+
export interface PaginatedGroups {
|
|
19
|
+
groups: ReportGroup[];
|
|
20
|
+
currentPage: number;
|
|
21
|
+
totalPages: number;
|
|
22
|
+
totalCount: number;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,UAAU,CAAA;AAEhF,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,YAAY,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,UAAU,CAAA;IACtB,QAAQ,EAAE,UAAU,EAAE,CAAA;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,WAAW,EAAE,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;CACnB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "strategic-review-webui",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Strategic Review Interactive Web Server",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"strategic-review-webui": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"views",
|
|
12
|
+
"public",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"dev": "ts-node src/index.ts",
|
|
20
|
+
"cli": "ts-node src/cli.ts",
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"start": "node dist/index.js",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"ejs": "^3.1.10",
|
|
27
|
+
"express": "^4.18.2",
|
|
28
|
+
"gray-matter": "^4.0.3",
|
|
29
|
+
"marked": "^9.1.6"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/ejs": "^3.1.5",
|
|
33
|
+
"@types/express": "^4.17.21",
|
|
34
|
+
"@types/node": "^20.11.0",
|
|
35
|
+
"ts-node": "^10.9.2",
|
|
36
|
+
"typescript": "^5.3.3"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/public/style.css
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
2
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f5; color: #333; line-height: 1.6; }
|
|
3
|
+
a { color: #0066cc; text-decoration: none; }
|
|
4
|
+
a:hover { text-decoration: underline; }
|
|
5
|
+
|
|
6
|
+
.nav { background: #1a1a2e; color: #fff; padding: 12px 24px; display: flex; align-items: center; gap: 16px; }
|
|
7
|
+
.nav h1 { font-size: 1.1rem; font-weight: 600; }
|
|
8
|
+
.nav a { color: #aab4ff; font-size: 0.9rem; }
|
|
9
|
+
|
|
10
|
+
.container { max-width: 960px; margin: 0 auto; padding: 24px 16px; }
|
|
11
|
+
|
|
12
|
+
.card { background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 20px; margin-bottom: 16px; }
|
|
13
|
+
.card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 8px; }
|
|
14
|
+
.card-title { font-size: 1.1rem; font-weight: 600; }
|
|
15
|
+
.card-meta { font-size: 0.85rem; color: #666; margin-top: 4px; }
|
|
16
|
+
|
|
17
|
+
.badge { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 0.78rem; font-weight: 600; }
|
|
18
|
+
.badge-init { background: #e8e8e8; color: #555; }
|
|
19
|
+
.badge-submit { background: #fff3cd; color: #856404; }
|
|
20
|
+
.badge-approve { background: #d1edde; color: #155724; }
|
|
21
|
+
.badge-reject { background: #f8d7da; color: #842029; }
|
|
22
|
+
.badge-revision { background: #e0d7f8; color: #5a2d82; }
|
|
23
|
+
|
|
24
|
+
.btn { display: inline-block; padding: 8px 18px; border-radius: 6px; border: none; cursor: pointer; font-size: 0.9rem; font-weight: 500; }
|
|
25
|
+
.btn-primary { background: #0066cc; color: #fff; }
|
|
26
|
+
.btn-primary:hover { background: #0052a3; }
|
|
27
|
+
.btn-success { background: #198754; color: #fff; }
|
|
28
|
+
.btn-success:hover { background: #146c43; }
|
|
29
|
+
.btn-danger { background: #dc3545; color: #fff; }
|
|
30
|
+
.btn-danger:hover { background: #b02a37; }
|
|
31
|
+
.btn-secondary { background: #6c757d; color: #fff; }
|
|
32
|
+
.btn-secondary:hover { background: #565e64; }
|
|
33
|
+
|
|
34
|
+
.form-group { margin-bottom: 16px; }
|
|
35
|
+
label { display: block; font-weight: 500; margin-bottom: 6px; font-size: 0.9rem; }
|
|
36
|
+
input[type=text], textarea { width: 100%; padding: 8px 12px; border: 1px solid #ccc; border-radius: 6px; font-size: 0.95rem; font-family: inherit; }
|
|
37
|
+
textarea { resize: vertical; min-height: 100px; }
|
|
38
|
+
input[type=text]:focus, textarea:focus { outline: none; border-color: #0066cc; box-shadow: 0 0 0 2px rgba(0,102,204,0.15); }
|
|
39
|
+
|
|
40
|
+
.meta-table { width: 100%; border-collapse: collapse; margin-bottom: 20px; font-size: 0.9rem; }
|
|
41
|
+
.meta-table th { width: 120px; text-align: left; padding: 8px 12px; background: #f8f8f8; border: 1px solid #e0e0e0; color: #555; font-weight: 500; }
|
|
42
|
+
.meta-table td { padding: 8px 12px; border: 1px solid #e0e0e0; }
|
|
43
|
+
|
|
44
|
+
.markdown-body { background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 24px; margin-bottom: 20px; }
|
|
45
|
+
.markdown-body h1, .markdown-body h2, .markdown-body h3 { margin: 1em 0 0.5em; font-weight: 600; }
|
|
46
|
+
.markdown-body h1 { font-size: 1.6rem; border-bottom: 2px solid #e0e0e0; padding-bottom: 8px; }
|
|
47
|
+
.markdown-body h2 { font-size: 1.3rem; }
|
|
48
|
+
.markdown-body h3 { font-size: 1.1rem; }
|
|
49
|
+
.markdown-body p { margin-bottom: 12px; }
|
|
50
|
+
.markdown-body ul, .markdown-body ol { margin: 0 0 12px 24px; }
|
|
51
|
+
.markdown-body li { margin-bottom: 4px; }
|
|
52
|
+
.markdown-body code { background: #f0f0f0; padding: 2px 6px; border-radius: 4px; font-size: 0.88em; font-family: 'Courier New', monospace; }
|
|
53
|
+
.markdown-body pre { background: #f0f0f0; padding: 16px; border-radius: 6px; overflow-x: auto; margin-bottom: 12px; }
|
|
54
|
+
.markdown-body pre code { background: none; padding: 0; }
|
|
55
|
+
.markdown-body blockquote { border-left: 4px solid #ccc; padding-left: 16px; color: #666; margin-bottom: 12px; }
|
|
56
|
+
.markdown-body table { border-collapse: collapse; width: 100%; margin-bottom: 12px; }
|
|
57
|
+
.markdown-body th, .markdown-body td { border: 1px solid #ddd; padding: 8px 12px; }
|
|
58
|
+
.markdown-body th { background: #f8f8f8; font-weight: 600; }
|
|
59
|
+
|
|
60
|
+
.version-nav { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 20px; }
|
|
61
|
+
.version-chip { padding: 4px 12px; border-radius: 16px; font-size: 0.85rem; border: 1px solid #ccc; background: #f8f8f8; }
|
|
62
|
+
.version-chip.active { background: #0066cc; color: #fff; border-color: #0066cc; }
|
|
63
|
+
|
|
64
|
+
.review-section { background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
|
|
65
|
+
.review-section h3 { margin-bottom: 16px; font-size: 1rem; color: #444; }
|
|
66
|
+
.review-actions { display: flex; gap: 12px; flex-wrap: wrap; }
|
|
67
|
+
|
|
68
|
+
.comment-box { background: #fff8e1; border: 1px solid #ffe082; border-radius: 6px; padding: 14px 16px; margin-bottom: 16px; }
|
|
69
|
+
.comment-box strong { color: #795548; }
|
|
70
|
+
|
|
71
|
+
.pagination { display: flex; gap: 8px; justify-content: center; margin-top: 24px; }
|
|
72
|
+
.pagination a, .pagination span { padding: 6px 14px; border: 1px solid #ccc; border-radius: 6px; font-size: 0.9rem; background: #fff; }
|
|
73
|
+
.pagination .active { background: #0066cc; color: #fff; border-color: #0066cc; }
|
|
74
|
+
.pagination a:hover { background: #f0f0f0; text-decoration: none; }
|
|
75
|
+
|
|
76
|
+
.error-page { text-align: center; padding: 80px 24px; }
|
|
77
|
+
.error-page h2 { font-size: 2rem; color: #dc3545; margin-bottom: 12px; }
|
|
78
|
+
.error-page p { color: #666; margin-bottom: 24px; }
|
|
79
|
+
|
|
80
|
+
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
|
81
|
+
.page-header h2 { font-size: 1.4rem; }
|
|
82
|
+
|
|
83
|
+
.alert { padding: 12px 16px; border-radius: 6px; margin-bottom: 16px; font-size: 0.9rem; }
|
|
84
|
+
.alert-error { background: #f8d7da; border: 1px solid #f5c2c7; color: #842029; }
|
|
85
|
+
|
|
86
|
+
.empty-state { text-align: center; padding: 60px 24px; color: #888; }
|
|
87
|
+
.empty-state p { margin-bottom: 16px; }
|
|
88
|
+
|
|
89
|
+
.command-guide { background: #f0f4ff; border: 1px solid #c7d4f5; border-radius: 8px; padding: 16px 20px; margin-bottom: 20px; }
|
|
90
|
+
.command-guide-text { font-size: 0.9rem; color: #444; margin-bottom: 10px; }
|
|
91
|
+
.command-snippet { display: flex; align-items: center; gap: 10px; background: #1a1a2e; border-radius: 6px; padding: 10px 14px; }
|
|
92
|
+
.command-snippet code { flex: 1; color: #aab4ff; font-family: 'Courier New', monospace; font-size: 0.9rem; }
|
|
93
|
+
.btn-copy { background: #3a3a5c; color: #aab4ff; border: 1px solid #5555aa; border-radius: 4px; padding: 4px 12px; font-size: 0.82rem; cursor: pointer; white-space: nowrap; }
|
|
94
|
+
.btn-copy:hover { background: #4a4a7c; }
|
|
95
|
+
|
|
96
|
+
.review-bar { position: fixed; bottom: 0; left: 0; right: 0; background: #fff; border-top: 2px solid #e0e0e0; box-shadow: 0 -2px 12px rgba(0,0,0,0.08); z-index: 100; max-height: 72px; overflow: hidden; transition: max-height 0.35s ease; }
|
|
97
|
+
.review-bar.expanded { max-height: 320px; }
|
|
98
|
+
.review-bar-inner { max-width: 960px; margin: 0 auto; padding: 12px 16px; display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
|
|
99
|
+
.review-bar-inner h3 { font-size: 0.95rem; color: #444; white-space: nowrap; margin-right: 4px; }
|
|
100
|
+
.review-actions { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; flex: 1; }
|
|
101
|
+
.body-has-review-bar { padding-bottom: 90px; }
|
|
102
|
+
.body-has-review-bar.review-expanded { padding-bottom: 340px; }
|
|
103
|
+
.review-expand { max-width: 960px; margin: 0 auto; padding: 0 16px 16px; opacity: 0; transition: opacity 0.2s ease 0.1s; }
|
|
104
|
+
.review-bar.expanded .review-expand { opacity: 1; }
|
|
105
|
+
.review-expand textarea { width: 100%; min-height: 130px; resize: vertical; padding: 10px 12px; border: 1px solid #ccc; border-radius: 6px; font-size: 0.9rem; font-family: inherit; line-height: 1.5; white-space: pre-wrap; }
|
|
106
|
+
.review-expand textarea:focus { outline: none; border-color: #0066cc; box-shadow: 0 0 0 2px rgba(0,102,204,0.15); }
|
|
107
|
+
.review-expand-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 8px; }
|
package/views/detail.ejs
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<%- include('layouts/head', { title: file.name || 'Report Detail' }) %>
|
|
2
|
+
|
|
3
|
+
<div class="page-header">
|
|
4
|
+
<h2><%= file.name || '(Unnamed)' %></h2>
|
|
5
|
+
<a href="/" class="btn btn-secondary">Back to List</a>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<table class="meta-table">
|
|
9
|
+
<tr><th>Filename</th><td><code><%= file.filename %></code></td></tr>
|
|
10
|
+
<tr><th>Status</th><td><%- include('partials/badge', { status: file.status }) %></td></tr>
|
|
11
|
+
<tr><th>Objective</th><td><%= file.objective %></td></tr>
|
|
12
|
+
<tr><th>Constraints</th><td><%= file.constraints || '-' %></td></tr>
|
|
13
|
+
</table>
|
|
14
|
+
|
|
15
|
+
<% if (file.reviewComment) { %>
|
|
16
|
+
<div class="comment-box"><strong>Review Comment:</strong> <%= file.reviewComment %></div>
|
|
17
|
+
<% } %>
|
|
18
|
+
|
|
19
|
+
<% if (file.status === 'init' || file.status === 'revision') { %>
|
|
20
|
+
<div class="command-guide">
|
|
21
|
+
<p class="command-guide-text">
|
|
22
|
+
<% if (file.status === 'init') { %>
|
|
23
|
+
To request a strategy review from the agent, enter the command below
|
|
24
|
+
<% } else { %>
|
|
25
|
+
To request a strategy revision from the agent, enter the command below
|
|
26
|
+
<% } %>
|
|
27
|
+
</p>
|
|
28
|
+
<div class="command-snippet">
|
|
29
|
+
<code id="cmd-text">/strategic-review-interactive <%= file.filename %></code>
|
|
30
|
+
<button class="btn-copy" onclick="copyCommand()">Copy</button>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<% } %>
|
|
34
|
+
|
|
35
|
+
<div style="margin-bottom:12px;">
|
|
36
|
+
<strong style="font-size:0.9rem;color:#555;">Version History</strong>
|
|
37
|
+
<div class="version-nav" style="margin-top:8px;">
|
|
38
|
+
<% for (const f of group.allFiles) { %>
|
|
39
|
+
<a href="/reports/<%= f.filename %>" class="version-chip <%= f.filename === file.filename ? 'active' : '' %>">v<%= f.version %> <%- include('partials/badge', { status: f.status }) %></a>
|
|
40
|
+
<% } %>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="markdown-body"><%- renderedContent %></div>
|
|
45
|
+
|
|
46
|
+
<% if (file.status === 'submit') { %>
|
|
47
|
+
<div class="review-bar" id="review-bar">
|
|
48
|
+
<div class="review-bar-inner">
|
|
49
|
+
<h3>Review</h3>
|
|
50
|
+
<div class="review-actions">
|
|
51
|
+
<button type="button" class="btn btn-success" onclick="openReview('approve')">Approve</button>
|
|
52
|
+
<button type="button" class="btn btn-danger" onclick="openReview('reject')">Reject</button>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="review-expand" id="review-expand">
|
|
56
|
+
<form id="review-form" method="POST" action="">
|
|
57
|
+
<textarea id="review-comment" name="comment" required rows="5"
|
|
58
|
+
placeholder="Write your review Examples: - Request an alternative strategic option analysis - Request detailed analysis of the Risk Assessment section - Request elaboration on the Implementation roadmap - Adopt Strategic Option A: [brief rationale] - Reject — insufficient justification in Decision Logic - Approve with condition: revise Success Criteria metrics"></textarea>
|
|
59
|
+
<div class="review-expand-actions">
|
|
60
|
+
<button type="button" class="btn btn-secondary" onclick="closeReview()">Cancel</button>
|
|
61
|
+
<button type="submit" id="review-submit-btn" class="btn btn-success">Confirm</button>
|
|
62
|
+
</div>
|
|
63
|
+
</form>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<script>document.body.classList.add('body-has-review-bar');</script>
|
|
67
|
+
<% } %>
|
|
68
|
+
|
|
69
|
+
<script>
|
|
70
|
+
function copyCommand() {
|
|
71
|
+
const text = document.getElementById('cmd-text').textContent;
|
|
72
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
73
|
+
const btn = document.querySelector('.btn-copy');
|
|
74
|
+
const original = btn.textContent;
|
|
75
|
+
btn.textContent = 'Copied!';
|
|
76
|
+
setTimeout(function() { btn.textContent = original; }, 2000);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
var pendingAction = null;
|
|
81
|
+
var approveUrl = '<%= "/reports/" + file.filename + "/approve" %>';
|
|
82
|
+
var rejectUrl = '<%= "/reports/" + file.filename + "/reject" %>';
|
|
83
|
+
|
|
84
|
+
function openReview(action) {
|
|
85
|
+
pendingAction = action;
|
|
86
|
+
var form = document.getElementById('review-form');
|
|
87
|
+
var submitBtn = document.getElementById('review-submit-btn');
|
|
88
|
+
if (action === 'approve') {
|
|
89
|
+
form.action = approveUrl;
|
|
90
|
+
submitBtn.textContent = 'Confirm Approve';
|
|
91
|
+
submitBtn.className = 'btn btn-success';
|
|
92
|
+
} else {
|
|
93
|
+
form.action = rejectUrl;
|
|
94
|
+
submitBtn.textContent = 'Confirm Reject';
|
|
95
|
+
submitBtn.className = 'btn btn-danger';
|
|
96
|
+
}
|
|
97
|
+
document.getElementById('review-bar').classList.add('expanded');
|
|
98
|
+
document.body.classList.add('review-expanded');
|
|
99
|
+
document.getElementById('review-comment').focus();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function closeReview() {
|
|
103
|
+
document.getElementById('review-bar').classList.remove('expanded');
|
|
104
|
+
document.body.classList.remove('review-expanded');
|
|
105
|
+
document.getElementById('review-comment').value = '';
|
|
106
|
+
pendingAction = null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
110
|
+
var form = document.getElementById('review-form');
|
|
111
|
+
if (form) {
|
|
112
|
+
form.addEventListener('submit', function(e) {
|
|
113
|
+
var comment = document.getElementById('review-comment').value.trim();
|
|
114
|
+
if (!comment) {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
document.getElementById('review-comment').focus();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
document.addEventListener('keydown', function(e) {
|
|
122
|
+
if (e.key === 'Escape') closeReview();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
</script>
|
|
126
|
+
|
|
127
|
+
<%- include('layouts/foot') %>
|
package/views/error.ejs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="ko">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title><%= title %> - Strategic Review</title>
|
|
7
|
+
<link rel="stylesheet" href="/style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<nav class="nav">
|
|
11
|
+
<h1>Strategic Review</h1>
|
|
12
|
+
<a href="/">Reports</a>
|
|
13
|
+
<a href="/reports/new">New Report</a>
|
|
14
|
+
</nav>
|
|
15
|
+
<div class="container">
|
package/views/list.ejs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<%- include('layouts/head', { title: 'Reports' }) %>
|
|
2
|
+
|
|
3
|
+
<div class="page-header">
|
|
4
|
+
<h2>Reports <small style="font-size:0.8em;color:#888;">(Total: <%= totalCount %>)</small></h2>
|
|
5
|
+
<a href="/reports/new" class="btn btn-primary">New Report</a>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<% if (groups.length === 0) { %>
|
|
9
|
+
<div class="empty-state">
|
|
10
|
+
<p>No reports found.</p>
|
|
11
|
+
<a href="/reports/new" class="btn btn-primary">Create First Report</a>
|
|
12
|
+
</div>
|
|
13
|
+
<% } else { %>
|
|
14
|
+
<% for (const group of groups) { %>
|
|
15
|
+
<% const f = group.latestFile %>
|
|
16
|
+
<% const versionCount = group.allFiles.length %>
|
|
17
|
+
<div class="card">
|
|
18
|
+
<div class="card-header">
|
|
19
|
+
<div>
|
|
20
|
+
<div class="card-title"><a href="/reports/<%= f.filename %>"><%= f.name || '(Unnamed)' %></a></div>
|
|
21
|
+
<div class="card-meta">
|
|
22
|
+
<%= f.prefix.replace('_', ' ') %> · v<%= f.version %> (<%= versionCount %> versions)
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
<%- include('partials/badge', { status: f.status }) %>
|
|
26
|
+
</div>
|
|
27
|
+
<div style="font-size:0.9rem;color:#555;margin-top:8px;"><%= f.objective.slice(0, 120) %><%= f.objective.length > 120 ? '...' : '' %></div>
|
|
28
|
+
</div>
|
|
29
|
+
<% } %>
|
|
30
|
+
<% } %>
|
|
31
|
+
|
|
32
|
+
<% if (totalPages > 1) { %>
|
|
33
|
+
<div class="pagination">
|
|
34
|
+
<% if (currentPage > 1) { %><a href="/?page=<%= currentPage - 1 %>">Previous</a><% } %>
|
|
35
|
+
<% for (let i = 1; i <= totalPages; i++) { %>
|
|
36
|
+
<% if (i === currentPage) { %>
|
|
37
|
+
<span class="active"><%= i %></span>
|
|
38
|
+
<% } else { %>
|
|
39
|
+
<a href="/?page=<%= i %>"><%= i %></a>
|
|
40
|
+
<% } %>
|
|
41
|
+
<% } %>
|
|
42
|
+
<% if (currentPage < totalPages) { %><a href="/?page=<%= currentPage + 1 %>">Next</a><% } %>
|
|
43
|
+
</div>
|
|
44
|
+
<% } %>
|
|
45
|
+
|
|
46
|
+
<%- include('layouts/foot') %>
|
package/views/new.ejs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<%- include('layouts/head', { title: 'New Report' }) %>
|
|
2
|
+
|
|
3
|
+
<div class="page-header">
|
|
4
|
+
<h2>New Report</h2>
|
|
5
|
+
<a href="/" class="btn btn-secondary">Back to List</a>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<% if (error) { %>
|
|
9
|
+
<div class="alert alert-error"><%= error %></div>
|
|
10
|
+
<% } %>
|
|
11
|
+
|
|
12
|
+
<div class="card">
|
|
13
|
+
<form method="POST" action="/reports">
|
|
14
|
+
<div class="form-group">
|
|
15
|
+
<label for="name">Report Name *</label>
|
|
16
|
+
<input type="text" id="name" name="name" required placeholder="e.g. Q1 Marketing Strategy Review">
|
|
17
|
+
</div>
|
|
18
|
+
<div class="form-group">
|
|
19
|
+
<label for="objective">Objective *</label>
|
|
20
|
+
<textarea id="objective" name="objective" required placeholder="Enter the objective of this report"></textarea>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="form-group">
|
|
23
|
+
<label for="constraints">Constraints</label>
|
|
24
|
+
<textarea id="constraints" name="constraints" placeholder="Enter any constraints if applicable"></textarea>
|
|
25
|
+
</div>
|
|
26
|
+
<div style="display:flex;gap:12px;">
|
|
27
|
+
<button type="submit" class="btn btn-primary">Create Report</button>
|
|
28
|
+
<a href="/" class="btn btn-secondary">Cancel</a>
|
|
29
|
+
</div>
|
|
30
|
+
</form>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<%- include('layouts/foot') %>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<%
|
|
2
|
+
const STATUS_LABEL = { init: 'Draft', submit: 'In Review', approve: 'Approved', reject: 'Rejected', revision: 'Revision' }
|
|
3
|
+
const STATUS_CLASS = { init: 'badge-init', submit: 'badge-submit', approve: 'badge-approve', reject: 'badge-reject', revision: 'badge-revision' }
|
|
4
|
+
%><span class="badge <%= STATUS_CLASS[status] %>"><%= STATUS_LABEL[status] %></span>
|