Skip to content

Commit dad3fe8

Browse files
committed
Add ui screen
1 parent 498bbbd commit dad3fe8

File tree

11 files changed

+1306
-277
lines changed

11 files changed

+1306
-277
lines changed

hubSyncHandler.log

Lines changed: 754 additions & 0 deletions
Large diffs are not rendered by default.

lib/hubSyncHandler.js

Lines changed: 244 additions & 248 deletions
Large diffs are not rendered by default.

lib/routes.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,54 @@ function setupRoutes (robot, getRouter) {
605605
}
606606
})
607607

608+
609+
// GET /api/safe-settings/hub/log
610+
// Returns parsed log entries (JSON): [{ timestamp, level, message }, ...]
611+
router.get('/api/safe-settings/hub/log', async (req, res) => {
612+
const lines = parseInt(req.query.lines || process.env.SAFE_SETTINGS_LOG_FILE_MAX_LINES || '1000', 10)
613+
const levelsQuery = req.query.levels // comma-separated e.g. 'ERROR,WARN'
614+
const allowedLevels = levelsQuery ? new Set(String(levelsQuery).split(',').map(s => s.trim().toUpperCase()).filter(Boolean)) : null
615+
616+
const candidates = []
617+
if (process.env.SAFE_SETTINGS_LOG_FILE) candidates.push(process.env.SAFE_SETTINGS_LOG_FILE)
618+
candidates.push(path.join(rootDir, 'safe-settings.log'))
619+
candidates.push(path.join(rootDir, '..', 'safe-settings.log'))
620+
candidates.push(path.join(rootDir, 'ui', 'safe-settings.log'))
621+
622+
let found = null
623+
for (const p of candidates) {
624+
if (!p) continue
625+
try {
626+
const st = await fs.promises.stat(p)
627+
if (st && st.isFile()) { found = p; break }
628+
} catch (e) {
629+
// ignore
630+
}
631+
}
632+
if (!found) return res.status(404).json({ error: 'Log file not found' })
633+
634+
try {
635+
const data = await fs.promises.readFile(found, 'utf8')
636+
const arr = data.split(/\r?\n/).filter(Boolean)
637+
const tail = arr.slice(-lines)
638+
const parsed = tail.map(line => {
639+
// Expecting format: 2025-09-10T12:34:56.789Z [INFO] message
640+
const m = line.match(/^(\d{4}-\d{2}-\d{2}T[^\s]+)\s+\[([A-Z]+)\]\s+(.*)$/)
641+
if (m) {
642+
return { timestamp: m[1], level: m[2], message: m[3], raw: line }
643+
}
644+
// fallback: try to extract level in brackets
645+
const m2 = line.match(/\[([A-Z]+)\]\s*(.*)$/)
646+
if (m2) return { timestamp: null, level: m2[1], message: m2[2], raw: line }
647+
return { timestamp: null, level: 'UNKNOWN', message: line, raw: line }
648+
})
649+
const filtered = allowedLevels ? parsed.filter(p => allowedLevels.has(String(p.level).toUpperCase())) : parsed
650+
return res.json({ count: filtered.length, entries: filtered })
651+
} catch (err) {
652+
return res.status(500).json({ error: err && err.message ? err.message : String(err) })
653+
}
654+
})
655+
608656
return router
609657
}
610658

package-lock.json

Lines changed: 27 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"eta": "^3.5.0",
3333
"js-yaml": "^4.1.0",
3434
"lodash": "^4.17.21",
35-
"minimatch": "^10.0.1",
35+
"minimatch": "^10.0.3",
3636
"next": "^15.5.2",
3737
"node-cron": "^3.0.2",
3838
"octokit": "^5.0.2",

safe-settings.log

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
2025-09-11T01:43:41.125Z [INFO] Received 'pull_request.closed' event: 45
2+
2025-09-11T01:43:41.125Z [INFO] Pull request closed on Safe-Settings Hub: (jefeish-training/safe-settings-config-master)
3+
2025-09-11T01:43:42.072Z [INFO] Files changed in PR #45: .github/safe-settings/globals/suborg.yml
4+
2025-09-11T01:43:42.072Z [DEBUG] Detected changes in the globals folder. Routing to syncHubGlobalsUpdate(...).
5+
2025-09-11T01:43:42.073Z [INFO] Syncing safe settings for 'globals/'.
6+
2025-09-11T01:43:42.359Z [DEBUG] Loaded manifest.yml rules from hub repo:{
7+
"rules": [
8+
{
9+
"name": "global-defaults",
10+
"targets": [
11+
"*"
12+
],
13+
"files": [
14+
"*.yml"
15+
],
16+
"mergeStrategy": "merge"
17+
},
18+
{
19+
"name": "security-policies",
20+
"targets": [
21+
"acme-*",
22+
"foo-bar"
23+
],
24+
"files": [
25+
"settings.yml"
26+
],
27+
"mergeStrategy": "overwrite"
28+
}
29+
]
30+
}
31+
2025-09-11T01:43:42.361Z [DEBUG] Evaluating globals file: .github/safe-settings/globals/suborg.yml
32+
2025-09-11T01:43:42.361Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jetest99' with mergeStrategy='merge'
33+
2025-09-11T01:43:42.361Z [DEBUG] Rule 'global-defaults' matches file 'suborg.yml'. Targets: jetest99, jefeish-training, jefeish-test1, copilot-for-emus, jefeish-migration-test, decyjphr-training, decyjphr-emu
34+
2025-09-11T01:43:42.988Z [DEBUG] Checking existence of .github/suborg.yml in jetest99/safe-settings-config
35+
2025-09-11T01:43:43.292Z [INFO] Skipping sync of suborg.yml to jetest99 (already exists & mergeStrategy=merge)
36+
2025-09-11T01:43:43.292Z [DEBUG] Found .github/suborg.yml in jetest99/safe-settings-config
37+
2025-09-11T01:43:43.292Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-training' with mergeStrategy='merge'
38+
2025-09-11T01:43:43.773Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-training/safe-settings-config
39+
2025-09-11T01:43:44.077Z [DEBUG] Found .github/suborg.yml in jefeish-training/safe-settings-config
40+
2025-09-11T01:43:44.078Z [INFO] Skipping sync of suborg.yml to jefeish-training (already exists & mergeStrategy=merge)
41+
2025-09-11T01:43:44.078Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-test1' with mergeStrategy='merge'
42+
2025-09-11T01:43:44.793Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-test1/safe-settings-config
43+
2025-09-11T01:43:45.082Z [DEBUG] Found .github/suborg.yml in jefeish-test1/safe-settings-config
44+
2025-09-11T01:43:45.082Z [INFO] Skipping sync of suborg.yml to jefeish-test1 (already exists & mergeStrategy=merge)
45+
2025-09-11T01:43:45.082Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'copilot-for-emus' with mergeStrategy='merge'
46+
2025-09-11T01:43:45.593Z [INFO] Skipping org copilot-for-emus: config repo 'safe-settings-config' does not exist.
47+
2025-09-11T01:43:45.593Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-migration-test' with mergeStrategy='merge'
48+
2025-09-11T01:43:46.208Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-migration-test/safe-settings-config
49+
2025-09-11T01:43:46.461Z [INFO] Skipping sync of suborg.yml to jefeish-migration-test (already exists & mergeStrategy=merge)
50+
2025-09-11T01:43:46.461Z [DEBUG] Found .github/suborg.yml in jefeish-migration-test/safe-settings-config
51+
2025-09-11T01:43:46.461Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'decyjphr-training' with mergeStrategy='merge'
52+
2025-09-11T01:43:46.897Z [INFO] Skipping org decyjphr-training: config repo 'safe-settings-config' does not exist.
53+
2025-09-11T01:43:46.897Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'decyjphr-emu' with mergeStrategy='merge'
54+
2025-09-11T01:43:47.342Z [INFO] Skipping org decyjphr-emu: config repo 'safe-settings-config' does not exist.

ui/public/favicon.ico

Whitespace-only changes.

ui/src/app/api/logs/route.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import fs from 'fs/promises'
2+
import path from 'path'
3+
4+
export const dynamic = 'force-static'
5+
6+
async function findLogFile () {
7+
const candidates = []
8+
if (process.env.SAFE_SETTINGS_LOG_FILE) candidates.push(process.env.SAFE_SETTINGS_LOG_FILE)
9+
candidates.push(path.join(process.cwd(), 'safe-settings.log'))
10+
candidates.push(path.join(process.cwd(), '..', 'safe-settings.log'))
11+
candidates.push(path.join(process.cwd(), '..', '..', 'safe-settings.log'))
12+
13+
for (const p of candidates) {
14+
if (!p) continue
15+
try {
16+
const st = await fs.stat(p)
17+
if (st && st.isFile()) return p
18+
} catch (e) {
19+
// ignore
20+
}
21+
}
22+
return null
23+
}
24+
25+
export async function GET () {
26+
const msg = 'Disabled in static export: use the backend endpoint /api/safe-settings/logs or set SAFE_SETTINGS_LOG_FILE to point at the log file.'
27+
return new Response(msg, { status: 200, headers: { 'content-type': 'text/plain; charset=utf-8' } })
28+
}

ui/src/app/components/TitleBar.jsx

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -108,21 +108,39 @@ export default function TitleBar() {
108108
)}
109109
</a>
110110
</li>
111+
<li className="nav-item">
112+
<a
113+
className={`btn btn-sm ms-auto nav-link fw-light d-flex position-relative menu-hover nav-link-custom${
114+
isDark ? " dark-font" : " light-font"
115+
}`}
116+
href="/dashboard/help"
117+
>
118+
<span className="me-1">
119+
<NoteIcon size={16} />
120+
</span>
121+
About
122+
{pathname === "/dashboard/help" && (
123+
<span className="menu-active-indicator"></span>
124+
)}
125+
</a>
126+
</li>
127+
<li className="nav-item">
128+
<a
129+
className={`btn btn-sm ms-auto nav-link fw-light d-flex position-relative menu-hover nav-link-custom${
130+
isDark ? " dark-font" : " light-font"
131+
}`}
132+
href="/dashboard/logs"
133+
>
134+
<span className="me-1">
135+
<NoteIcon size={16} />
136+
</span>
137+
Sync-Logs
138+
{pathname === "/dashboard/logs" && (
139+
<span className="menu-active-indicator"></span>
140+
)}
141+
</a>
142+
</li>
111143
</ul>
112-
<a
113-
className={`btn btn-sm ms-auto nav-link fw-light d-flex position-relative menu-hover nav-link-custom${
114-
isDark ? " dark-font" : " light-font"
115-
}`}
116-
href="/dashboard/help"
117-
>
118-
<span className="me-1">
119-
<NoteIcon size={16} />
120-
</span>
121-
About
122-
{pathname === "/dashboard/help" && (
123-
<span className="menu-active-indicator"></span>
124-
)}
125-
</a>
126144
</div>
127145
</nav>
128146
</>

ui/src/app/dashboard/logs/page.jsx

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"use client"
2+
import TitleBar from '../../components/TitleBar'
3+
import { useState } from 'react'
4+
5+
export default function LogsPage () {
6+
// Static mock data for demonstration
7+
const mockEntries = [
8+
{ timestamp: '2025-09-11T10:00:00.000Z', level: 'INFO', message: 'Safe Settings service started.' },
9+
{ timestamp: '2025-09-11T10:01:05.123Z', level: 'WARN', message: 'Config file missing, using defaults.' },
10+
{ timestamp: '2025-09-11T10:02:10.456Z', level: 'ERROR', message: 'Failed to sync settings: network error.' },
11+
{ timestamp: '2025-09-11T10:03:00.789Z', level: 'DEBUG', message: 'Polling GitHub API for updates.' },
12+
{ timestamp: '2025-09-11T10:04:15.000Z', level: 'INFO', message: 'Sync completed successfully.' },
13+
{ timestamp: '2025-09-11T10:05:00.000Z', level: 'INFO', message: 'SYNC: Organization settings updated.' },
14+
{ timestamp: '2025-09-11T10:06:00.000Z', level: 'ERROR', message: 'SYNC: Failed to update organization settings.' }
15+
]
16+
17+
const logLevels = ['INFO', 'WARN', 'DEBUG', 'ERROR']
18+
const [selectedLevels, setSelectedLevels] = useState(new Set(logLevels))
19+
const [search, setSearch] = useState('')
20+
21+
const toggleLevel = (lvl) => {
22+
const next = new Set(selectedLevels)
23+
if (next.has(lvl)) next.delete(lvl)
24+
else next.add(lvl)
25+
setSelectedLevels(next)
26+
}
27+
28+
const filtered = mockEntries.filter(e =>
29+
selectedLevels.has(e.level.toUpperCase()) &&
30+
(search.trim() === '' || e.message.toLowerCase().includes(search.trim().toLowerCase()))
31+
)
32+
33+
return (
34+
<>
35+
<TitleBar />
36+
<div className="container py-4">
37+
<div className="col-12 mb-4">
38+
<div className="card shadow-sm">
39+
<div className="card-body">
40+
<h4 className="card-title mb-2">Safe Settings Log</h4>
41+
<p className="card-text text-muted">View recent log entries for Safe Settings operations and syncs.</p>
42+
</div>
43+
</div>
44+
</div>
45+
<div className="col-12 mb-4">
46+
<div className="card shadow-sm">
47+
<div className="card-body">
48+
<h5 className="card-title mb-3">Filter Options</h5>
49+
<div className="mb-2">
50+
<strong>Log Levels:</strong>
51+
<div className="d-flex gap-3 mt-2">
52+
{logLevels.map(lvl => (
53+
<label key={lvl} className="form-check form-check-inline">
54+
<input className="form-check-input" type="checkbox" checked={selectedLevels.has(lvl)} onChange={() => toggleLevel(lvl)} />
55+
<span className="form-check-label">{lvl}</span>
56+
</label>
57+
))}
58+
</div>
59+
</div>
60+
<div className="mt-3">
61+
<strong>Search Message:</strong>
62+
<input
63+
type="text"
64+
className="form-control mt-1"
65+
placeholder="Search for SYNC, error, etc."
66+
value={search}
67+
onChange={e => setSearch(e.target.value)}
68+
style={{ maxWidth: 300 }}
69+
/>
70+
</div>
71+
</div>
72+
</div>
73+
</div>
74+
<div className="col-12">
75+
<div className="card shadow-sm">
76+
<div className="card-body">
77+
<h5 className="card-title mb-3">Log Entries</h5>
78+
<div className="table-responsive" style={{ maxHeight: '60vh', overflow: 'auto' }}>
79+
<table className="table table-sm table-striped align-middle">
80+
<thead>
81+
<tr>
82+
<th style={{width: '200px'}}>Timestamp</th>
83+
<th style={{width: '90px'}}>Level</th>
84+
<th>Message</th>
85+
</tr>
86+
</thead>
87+
<tbody>
88+
{filtered.map((row, i) => {
89+
let levelClass = ''
90+
if (row.level === 'ERROR') levelClass = 'log-error'
91+
else if (row.level === 'WARN') levelClass = 'log-warn'
92+
return (
93+
<tr key={`${row.timestamp || 'na'}-${i}`}>
94+
<td style={{fontSize: '0.85rem', whiteSpace: 'nowrap'}}>{row.timestamp || '-'}</td>
95+
<td className={levelClass} style={{fontWeight: 600}}>{row.level || 'UNKNOWN'}</td>
96+
<td className={levelClass} style={{fontFamily: 'monospace', fontSize: '0.9rem', whiteSpace: 'pre-wrap'}}>{row.message}</td>
97+
</tr>
98+
)
99+
})}
100+
</tbody>
101+
</table>
102+
{filtered.length === 0 && <div className="text-muted py-3">No log entries match your filters.</div>}
103+
</div>
104+
</div>
105+
</div>
106+
</div>
107+
</div>
108+
</>
109+
)
110+
}

0 commit comments

Comments
 (0)