Skip to content

Commit b8b1e39

Browse files
committed
feat: add diagnostic tools to identify student access issues
- Create migration utilities to query all notes and find orphaned data - Add global statistics view to diagnostic page - Show total notes, notes with subjects, orphaned notes, and invalid structure - Enable identification of notes missing metadata that students cannot see - Fix TypeScript configuration for better module resolution
1 parent 8de6bdc commit b8b1e39

3 files changed

Lines changed: 225 additions & 2 deletions

File tree

.vscode/settings.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
{
22
"typescript.tsdk": "node_modules/typescript/lib",
3-
"typescript.enablePromptUseWorkspaceTsdk": true
3+
"typescript.enablePromptUseWorkspaceTsdk": true,
4+
"typescript.preferences.importModuleSpecifier": "non-relative",
5+
"typescript.updateImportsOnFileMove.enabled": "always",
6+
"files.exclude": {
7+
"**/.next": true,
8+
"**/node_modules": true
9+
}
410
}

src/app/db-diagnostic/page.tsx

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
getSubjects,
99
getNotes
1010
} from "@/lib/firebase/firestore";
11+
import { getAllNotes, getNoteStatistics, findOrphanedNotes } from "@/lib/firebase/migrations";
1112

1213
export default function DBDiagnosticPage() {
1314
const [departments, setDepartments] = useState<any[]>([]);
@@ -22,6 +23,12 @@ export default function DBDiagnosticPage() {
2223
const [loading, setLoading] = useState(false);
2324
const [error, setError] = useState<string>("");
2425

26+
// Global diagnostics
27+
const [allNotes, setAllNotes] = useState<any[]>([]);
28+
const [statistics, setStatistics] = useState<any>(null);
29+
const [orphanedNotes, setOrphanedNotes] = useState<any[]>([]);
30+
const [showGlobalView, setShowGlobalView] = useState(false);
31+
2532
useEffect(() => {
2633
loadDepartments();
2734
}, []);
@@ -40,6 +47,27 @@ export default function DBDiagnosticPage() {
4047
}
4148
}
4249

50+
async function loadGlobalStatistics() {
51+
try {
52+
setLoading(true);
53+
setError("");
54+
const [stats, orphaned, all] = await Promise.all([
55+
getNoteStatistics(),
56+
findOrphanedNotes(),
57+
getAllNotes()
58+
]);
59+
setStatistics(stats);
60+
setOrphanedNotes(orphaned);
61+
setAllNotes(all);
62+
setShowGlobalView(true);
63+
} catch (err: any) {
64+
console.error("Error loading statistics:", err);
65+
setError(err.message);
66+
} finally {
67+
setLoading(false);
68+
}
69+
}
70+
4371
async function loadBatches(deptId: string) {
4472
try {
4573
setLoading(true);
@@ -122,7 +150,23 @@ export default function DBDiagnosticPage() {
122150

123151
return (
124152
<div style={{ padding: "2rem", maxWidth: "1200px", margin: "0 auto" }}>
125-
<h1 style={{ marginBottom: "2rem" }}>🔍 Firebase Database Diagnostic</h1>
153+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "2rem" }}>
154+
<h1>🔍 Firebase Database Diagnostic</h1>
155+
<button
156+
onClick={loadGlobalStatistics}
157+
style={{
158+
padding: "0.75rem 1.5rem",
159+
background: "#3b82f6",
160+
color: "white",
161+
border: "none",
162+
borderRadius: "6px",
163+
cursor: "pointer",
164+
fontWeight: "600"
165+
}}
166+
>
167+
📊 Show Global Statistics
168+
</button>
169+
</div>
126170

127171
{error && (
128172
<div style={{
@@ -139,6 +183,80 @@ export default function DBDiagnosticPage() {
139183

140184
{loading && <p>Loading...</p>}
141185

186+
{/* Global Statistics Panel */}
187+
{showGlobalView && statistics && (
188+
<div style={{ marginBottom: "2rem" }}>
189+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "1rem" }}>
190+
<h2>📊 Global Database Statistics</h2>
191+
<button
192+
onClick={() => setShowGlobalView(false)}
193+
style={{
194+
padding: "0.5rem 1rem",
195+
background: "#6b7280",
196+
color: "white",
197+
border: "none",
198+
borderRadius: "6px",
199+
cursor: "pointer"
200+
}}
201+
>
202+
Hide
203+
</button>
204+
</div>
205+
206+
{/* Statistics Cards */}
207+
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))", gap: "1rem", marginBottom: "2rem" }}>
208+
<div style={{ background: "#eff6ff", padding: "1.5rem", borderRadius: "8px", border: "1px solid #bfdbfe" }}>
209+
<h3 style={{ fontSize: "2rem", marginBottom: "0.5rem", color: "#1e40af" }}>{statistics.total}</h3>
210+
<p style={{ color: "#3b82f6", fontWeight: "600" }}>Total Notes</p>
211+
</div>
212+
<div style={{ background: "#d1fae5", padding: "1.5rem", borderRadius: "8px", border: "1px solid #6ee7b7" }}>
213+
<h3 style={{ fontSize: "2rem", marginBottom: "0.5rem", color: "#065f46" }}>{statistics.withSubject}</h3>
214+
<p style={{ color: "#10b981", fontWeight: "600" }}>With Subject</p>
215+
</div>
216+
<div style={{ background: "#fee2e2", padding: "1.5rem", borderRadius: "8px", border: "1px solid #fca5a5" }}>
217+
<h3 style={{ fontSize: "2rem", marginBottom: "0.5rem", color: "#991b1b" }}>{statistics.orphaned}</h3>
218+
<p style={{ color: "#ef4444", fontWeight: "600" }}>Orphaned (No Subject)</p>
219+
</div>
220+
<div style={{ background: "#fef3c7", padding: "1.5rem", borderRadius: "8px", border: "1px solid #fcd34d" }}>
221+
<h3 style={{ fontSize: "2rem", marginBottom: "0.5rem", color: "#92400e" }}>{statistics.invalid}</h3>
222+
<p style={{ color: "#f59e0b", fontWeight: "600" }}>Invalid Structure</p>
223+
</div>
224+
</div>
225+
226+
{/* Orphaned Notes Details */}
227+
{orphanedNotes.length > 0 && (
228+
<div style={{ background: "#fee2e2", padding: "1.5rem", borderRadius: "8px", marginBottom: "2rem" }}>
229+
<h3 style={{ color: "#991b1b", marginBottom: "1rem" }}>⚠️ Orphaned Notes ({orphanedNotes.length})</h3>
230+
<p style={{ marginBottom: "1rem", color: "#7f1d1d" }}>
231+
These files exist but are missing critical metadata. Students cannot see them.
232+
</p>
233+
<div style={{ maxHeight: "300px", overflow: "auto" }}>
234+
{orphanedNotes.map((note, idx) => (
235+
<div key={note.id} style={{ background: "white", padding: "1rem", marginBottom: "0.5rem", borderRadius: "6px" }}>
236+
<p style={{ fontWeight: "600", marginBottom: "0.5rem" }}>{idx + 1}. {note.title}</p>
237+
<p style={{ fontSize: "0.85rem", color: "#6b7280" }}>
238+
ID: {note.id}<br />
239+
Missing: {!note.subjectId && "subjectId "}
240+
{!note.departmentId && "departmentId "}
241+
{!note.batchId && "batchId "}
242+
{!note.semesterId && "semesterId"}
243+
</p>
244+
</div>
245+
))}
246+
</div>
247+
</div>
248+
)}
249+
250+
{/* All Notes */}
251+
<div style={{ background: "#f9fafb", padding: "1.5rem", borderRadius: "8px" }}>
252+
<h3 style={{ marginBottom: "1rem" }}>📄 All Notes in Database ({allNotes.length})</h3>
253+
<pre style={{ background: "#f5f5f5", padding: "1rem", borderRadius: "6px", overflow: "auto", maxHeight: "400px", fontSize: "0.85rem" }}>
254+
{JSON.stringify(allNotes, null, 2)}
255+
</pre>
256+
</div>
257+
</div>
258+
)}
259+
142260
<div style={{ display: "grid", gap: "2rem" }}>
143261
{/* Departments */}
144262
<div>

src/lib/firebase/migrations.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { db } from "./config";
2+
import {
3+
collection,
4+
getDocs,
5+
query,
6+
where,
7+
updateDoc,
8+
doc
9+
} from "firebase/firestore";
10+
11+
const NOTES = "notes";
12+
13+
/**
14+
* Get ALL notes in the database (no filtering)
15+
* Useful for diagnostics and identifying orphaned data
16+
*/
17+
export const getAllNotes = async () => {
18+
const snapshot = await getDocs(collection(db, NOTES));
19+
return snapshot.docs.map(d => ({ id: d.id, ...d.data() } as any));
20+
};
21+
22+
/**
23+
* Get notes that are missing critical metadata fields
24+
*/
25+
export const findOrphanedNotes = async () => {
26+
const allNotes = await getAllNotes();
27+
return allNotes.filter((note: any) =>
28+
!note.subjectId ||
29+
!note.departmentId ||
30+
!note.batchId ||
31+
!note.semesterId
32+
);
33+
};
34+
35+
/**
36+
* Validate note structure
37+
*/
38+
export const validateNoteStructure = (note: any) => {
39+
const requiredFields = ['subjectId', 'departmentId', 'batchId', 'semesterId', 'title', 'fileUrl', 'fileType'];
40+
const missingFields = requiredFields.filter(field => !note[field]);
41+
42+
return {
43+
valid: missingFields.length === 0,
44+
missingFields,
45+
note
46+
};
47+
};
48+
49+
/**
50+
* Get notes grouped by subject
51+
*/
52+
export const getNotesBySubject = async () => {
53+
const allNotes = await getAllNotes();
54+
const grouped = new Map<string, any[]>();
55+
56+
allNotes.forEach((note: any) => {
57+
const subjectId = note.subjectId || 'orphaned';
58+
if (!grouped.has(subjectId)) {
59+
grouped.set(subjectId, []);
60+
}
61+
grouped.get(subjectId)!.push(note);
62+
});
63+
64+
return Object.fromEntries(grouped);
65+
};
66+
67+
/**
68+
* Repair note metadata by updating missing fields
69+
*/
70+
export const repairNoteMetadata = async (noteId: string, metadata: {
71+
subjectId?: string;
72+
departmentId?: string;
73+
batchId?: string;
74+
semesterId?: string;
75+
}) => {
76+
const noteRef = doc(db, NOTES, noteId);
77+
await updateDoc(noteRef, metadata);
78+
return { noteId, updated: metadata };
79+
};
80+
81+
/**
82+
* Get detailed statistics about notes
83+
*/
84+
export const getNoteStatistics = async () => {
85+
const allNotes = await getAllNotes();
86+
const orphaned = allNotes.filter((n: any) => !n.subjectId);
87+
const withSubject = allNotes.filter((n: any) => n.subjectId);
88+
89+
const validationResults = allNotes.map(validateNoteStructure);
90+
const invalid = validationResults.filter(r => !r.valid);
91+
92+
return {
93+
total: allNotes.length,
94+
withSubject: withSubject.length,
95+
orphaned: orphaned.length,
96+
invalid: invalid.length,
97+
invalidNotes: invalid
98+
};
99+
};

0 commit comments

Comments
 (0)