Fix PDF generation failures from malformed AI responses
- Add OpenRouter response-healing plugin to auto-fix malformed JSON - Add stream: false to all OpenRouter API calls (required for plugin) - Add Zod schema validation before PDF generation with AI repair fallback - Fix CUID2 ID generation for skills (was generating invalid "skill-0") - Update idSchema to validate CUID2 format matching RXResume's validation - Save failed JSON and screenshots to data/errors/ for debugging - Add tests for CUID2 validation to prevent regression
This commit is contained in:
parent
1ca459ec34
commit
322feb8b67
@ -2,6 +2,9 @@
|
||||
* Shared OpenRouter API helper for structured JSON responses.
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { getSetting } from '../repositories/settings.js';
|
||||
|
||||
const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||
|
||||
export interface JsonSchemaDefinition {
|
||||
@ -75,6 +78,7 @@ export async function callOpenRouter<T>(
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
messages,
|
||||
stream: false,
|
||||
response_format: {
|
||||
type: 'json_schema',
|
||||
json_schema: {
|
||||
@ -83,6 +87,7 @@ export async function callOpenRouter<T>(
|
||||
schema: jsonSchema.schema,
|
||||
},
|
||||
},
|
||||
plugins: [{ id: 'response-healing' }],
|
||||
}),
|
||||
});
|
||||
|
||||
@ -165,3 +170,118 @@ export function parseJsonContent<T>(content: string, jobId?: string): T {
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate JSON against a Zod schema and repair with AI if invalid.
|
||||
*
|
||||
* @param data - The JSON object to validate
|
||||
* @param schema - Zod schema to validate against
|
||||
* @param context - Optional context for logging (e.g., job ID)
|
||||
* @returns The validated (and possibly repaired) data
|
||||
*/
|
||||
export async function validateAndRepairJson<T>(
|
||||
data: unknown,
|
||||
schema: z.ZodSchema<T>,
|
||||
context?: string
|
||||
): Promise<{ success: true; data: T; repaired: boolean } | { success: false; error: string }> {
|
||||
const label = context ?? 'unknown';
|
||||
|
||||
// First attempt: validate as-is
|
||||
const result = schema.safeParse(data);
|
||||
if (result.success) {
|
||||
return { success: true, data: result.data, repaired: false };
|
||||
}
|
||||
|
||||
// Validation failed - attempt AI repair
|
||||
console.warn(`⚠️ [${label}] Schema validation failed, attempting AI repair...`);
|
||||
|
||||
const errors = result.error.issues.map((issue: z.ZodIssue) => ({
|
||||
path: issue.path.join('.'),
|
||||
message: issue.message,
|
||||
code: issue.code,
|
||||
}));
|
||||
|
||||
console.warn(` Validation errors:`, errors.slice(0, 5)); // Log first 5 errors
|
||||
|
||||
// Check if API key is available
|
||||
if (!process.env.OPENROUTER_API_KEY) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Schema validation failed and no API key for repair: ${errors.map((e: { path: string; message: string }) => `${e.path}: ${e.message}`).join('; ')}`
|
||||
};
|
||||
}
|
||||
|
||||
const [overrideModel] = await Promise.all([getSetting('model')]);
|
||||
const model = overrideModel || process.env.MODEL || 'openai/gpt-4o-mini';
|
||||
|
||||
const repairPrompt = buildRepairPrompt(data, errors);
|
||||
|
||||
try {
|
||||
const response = await fetch(OPENROUTER_API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
'HTTP-Referer': 'JobOps',
|
||||
'X-Title': 'JobOpsSchemaRepair',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
messages: [{ role: 'user', content: repairPrompt }],
|
||||
stream: false,
|
||||
plugins: [{ id: 'response-healing' }],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text().catch(() => 'No error body');
|
||||
return { success: false, error: `AI repair request failed: ${response.status} - ${errorBody}` };
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
const content = responseData.choices?.[0]?.message?.content;
|
||||
|
||||
if (!content) {
|
||||
return { success: false, error: 'AI repair returned no content' };
|
||||
}
|
||||
|
||||
// Parse the repaired JSON
|
||||
const repaired = parseJsonContent<unknown>(content, label);
|
||||
|
||||
// Validate the repaired version
|
||||
const repairedResult = schema.safeParse(repaired);
|
||||
if (repairedResult.success) {
|
||||
console.log(`✅ [${label}] AI successfully repaired the JSON`);
|
||||
return { success: true, data: repairedResult.data, repaired: true };
|
||||
}
|
||||
|
||||
// Still invalid after repair
|
||||
const newErrors = repairedResult.error.issues.slice(0, 3).map((i: z.ZodIssue) => `${i.path.join('.')}: ${i.message}`).join('; ');
|
||||
return { success: false, error: `AI repair did not fix all issues: ${newErrors}` };
|
||||
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return { success: false, error: `AI repair failed: ${message}` };
|
||||
}
|
||||
}
|
||||
|
||||
function buildRepairPrompt(data: unknown, errors: Array<{ path: string; message: string; code: string }>): string {
|
||||
const errorList = errors.slice(0, 10).map(e => `- Path "${e.path}": ${e.message}`).join('\n');
|
||||
|
||||
return `You are fixing a JSON object that failed schema validation.
|
||||
|
||||
VALIDATION ERRORS:
|
||||
${errorList}
|
||||
|
||||
ORIGINAL JSON (may be truncated):
|
||||
${JSON.stringify(data, null, 2).slice(0, 15000)}
|
||||
|
||||
INSTRUCTIONS:
|
||||
1. Fix ONLY the validation errors listed above
|
||||
2. Do NOT remove or modify data that isn't causing errors
|
||||
3. For missing required fields, add them with sensible defaults (empty strings, empty arrays, etc.)
|
||||
4. For type mismatches, convert to the correct type
|
||||
5. Preserve all existing valid data
|
||||
|
||||
Return ONLY the fixed JSON object, no explanation.`;
|
||||
}
|
||||
|
||||
@ -61,6 +61,14 @@ vi.mock('./resumeProjects.js', () => ({
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('./openrouter.js', () => ({
|
||||
validateAndRepairJson: vi.fn().mockImplementation(async (data: unknown) => ({
|
||||
success: true,
|
||||
data,
|
||||
repaired: false
|
||||
}))
|
||||
}));
|
||||
|
||||
vi.mock('child_process', () => ({
|
||||
spawn: vi.fn().mockImplementation(() => ({
|
||||
stdout: { on: vi.fn() },
|
||||
@ -128,7 +136,7 @@ describe('PDF Service Skills Validation', () => {
|
||||
});
|
||||
|
||||
it('should sanitize base resume even if no skills are tailored', async () => {
|
||||
// Mock profile has an invalid skill (missing visible/description in the raw json implied,
|
||||
// Mock profile has an invalid skill (missing visible/description in the raw json implied,
|
||||
// though our mock above has them. Let's make a truly invalid one locally)
|
||||
const invalidProfile = {
|
||||
...mockProfile,
|
||||
@ -157,4 +165,101 @@ describe('PDF Service Skills Validation', () => {
|
||||
expect(item.description).toBe('');
|
||||
expect(item.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should generate CUID2-compatible IDs for skills without IDs', async () => {
|
||||
// Profile with skills missing IDs (common when AI generates them)
|
||||
const profileWithoutIds = {
|
||||
...mockProfile,
|
||||
sections: {
|
||||
...mockProfile.sections,
|
||||
skills: {
|
||||
items: [
|
||||
{ name: 'Skill 1', keywords: ['a'] },
|
||||
{ name: 'Skill 2', keywords: ['b'] },
|
||||
{ name: 'Skill 3', keywords: ['c'] }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
mocks.readFile.mockResolvedValueOnce(JSON.stringify(profileWithoutIds));
|
||||
|
||||
await generatePdf('job-cuid2-test', {}, 'Job Desc', 'dummy.json');
|
||||
|
||||
expect(mocks.writeFile).toHaveBeenCalled();
|
||||
const callArgs = mocks.writeFile.mock.calls[0];
|
||||
const savedResumeJson = JSON.parse(callArgs[1] as string);
|
||||
|
||||
const skillItems = savedResumeJson.sections.skills.items;
|
||||
|
||||
// All skills should have IDs
|
||||
skillItems.forEach((skill: any, index: number) => {
|
||||
expect(skill.id).toBeDefined();
|
||||
expect(typeof skill.id).toBe('string');
|
||||
expect(skill.id.length).toBeGreaterThanOrEqual(20);
|
||||
|
||||
// CUID2 format: starts with a letter, lowercase alphanumeric
|
||||
expect(skill.id).toMatch(/^[a-z][a-z0-9]+$/);
|
||||
});
|
||||
|
||||
// IDs should be unique
|
||||
const ids = skillItems.map((s: any) => s.id);
|
||||
const uniqueIds = new Set(ids);
|
||||
expect(uniqueIds.size).toBe(ids.length);
|
||||
});
|
||||
|
||||
it('should NOT generate IDs like "skill-0" which are invalid CUID2', async () => {
|
||||
const profileWithoutIds = {
|
||||
...mockProfile,
|
||||
sections: {
|
||||
...mockProfile.sections,
|
||||
skills: {
|
||||
items: [
|
||||
{ name: 'Skill Without ID', keywords: ['test'] }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
mocks.readFile.mockResolvedValueOnce(JSON.stringify(profileWithoutIds));
|
||||
|
||||
await generatePdf('job-no-skill-prefix', {}, 'Job Desc', 'dummy.json');
|
||||
|
||||
expect(mocks.writeFile).toHaveBeenCalled();
|
||||
const callArgs = mocks.writeFile.mock.calls[0];
|
||||
const savedResumeJson = JSON.parse(callArgs[1] as string);
|
||||
|
||||
const skill = savedResumeJson.sections.skills.items[0];
|
||||
|
||||
// ID should NOT be in the old invalid format
|
||||
expect(skill.id).not.toMatch(/^skill-\d+$/);
|
||||
|
||||
// Should be valid CUID2 format
|
||||
expect(skill.id).toMatch(/^[a-z][a-z0-9]+$/);
|
||||
});
|
||||
|
||||
it('should preserve existing valid IDs and not regenerate them', async () => {
|
||||
const validCuid2Id = 'ck9w4ygzq0000xmn5h0jt7l5c';
|
||||
const profileWithValidId = {
|
||||
...mockProfile,
|
||||
sections: {
|
||||
...mockProfile.sections,
|
||||
skills: {
|
||||
items: [
|
||||
{ id: validCuid2Id, name: 'Skill With Valid ID', keywords: ['test'], visible: true, description: '', level: 1 }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
mocks.readFile.mockResolvedValueOnce(JSON.stringify(profileWithValidId));
|
||||
|
||||
await generatePdf('job-preserve-id', {}, 'Job Desc', 'dummy.json');
|
||||
|
||||
expect(mocks.writeFile).toHaveBeenCalled();
|
||||
const callArgs = mocks.writeFile.mock.calls[0];
|
||||
const savedResumeJson = JSON.parse(callArgs[1] as string);
|
||||
|
||||
const skill = savedResumeJson.sections.skills.items[0];
|
||||
|
||||
// Should preserve the original valid ID
|
||||
expect(skill.id).toBe(validCuid2Id);
|
||||
});
|
||||
});
|
||||
|
||||
@ -8,15 +8,38 @@ import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { readFile, writeFile, mkdir, access, unlink } from 'fs/promises';
|
||||
import { existsSync } from 'fs';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
import { getSetting } from '../repositories/settings.js';
|
||||
import { pickProjectIdsForJob } from './projectSelection.js';
|
||||
import { extractProjectsFromProfile, resolveResumeProjectsSettings } from './resumeProjects.js';
|
||||
import { getDataDir } from '../config/dataDir.js';
|
||||
import { getProfile } from './profile.js';
|
||||
import { validateAndRepairJson } from './openrouter.js';
|
||||
import { resumeDataSchema } from '../../shared/rxresume-schema.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
/**
|
||||
* Generate a CUID2-compatible ID for RXResume.
|
||||
* CUID2 format: starts with a letter, lowercase alphanumeric, ~24 chars
|
||||
*/
|
||||
function generateCuid2(): string {
|
||||
const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const letters = 'abcdefghijklmnopqrstuvwxyz';
|
||||
const bytes = crypto.randomBytes(24);
|
||||
|
||||
// First char must be a letter
|
||||
let result = letters[bytes[0] % letters.length];
|
||||
|
||||
// Rest can be alphanumeric
|
||||
for (let i = 1; i < 24; i++) {
|
||||
result += alphabet[bytes[i] % alphabet.length];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Paths - can be overridden via env for Docker
|
||||
const RESUME_GEN_DIR = process.env.RESUME_GEN_DIR || join(__dirname, '../../../../resume-generator');
|
||||
const OUTPUT_DIR = join(getDataDir(), 'pdfs');
|
||||
@ -67,9 +90,9 @@ export async function generatePdf(
|
||||
// Sanitize skills: Ensure all skills have required schema fields (visible, description, id, level, keywords)
|
||||
// This fixes issues where the base JSON uses a shorthand format (missing required fields)
|
||||
if (baseResume.sections?.skills?.items && Array.isArray(baseResume.sections.skills.items)) {
|
||||
baseResume.sections.skills.items = baseResume.sections.skills.items.map((skill: any, index: number) => ({
|
||||
baseResume.sections.skills.items = baseResume.sections.skills.items.map((skill: any) => ({
|
||||
...skill,
|
||||
id: skill.id || `skill-${index}`,
|
||||
id: skill.id || generateCuid2(),
|
||||
visible: skill.visible ?? true,
|
||||
// Zod schema requires string, default to empty string if missing
|
||||
description: skill.description ?? '',
|
||||
@ -107,12 +130,12 @@ export async function generatePdf(
|
||||
if (newSkills && baseResume.sections?.skills) {
|
||||
// Ensure each skill item has required schema fields
|
||||
const existingSkills = baseResume.sections.skills.items || [];
|
||||
const skillsWithSchema = newSkills.map((newSkill: any, index: number) => {
|
||||
const skillsWithSchema = newSkills.map((newSkill: any) => {
|
||||
// Try to find matching existing skill to preserve id and other fields
|
||||
const existing = existingSkills.find((s: any) => s.name === newSkill.name);
|
||||
|
||||
return {
|
||||
id: newSkill.id || existing?.id || `skill-${index}`,
|
||||
id: newSkill.id || existing?.id || generateCuid2(),
|
||||
visible: newSkill.visible !== undefined ? newSkill.visible : (existing?.visible ?? true),
|
||||
name: newSkill.name || existing?.name || '',
|
||||
description: newSkill.description !== undefined ? newSkill.description : (existing?.description || ''),
|
||||
@ -165,9 +188,22 @@ export async function generatePdf(
|
||||
console.warn(` ⚠️ Project visibility step failed for job ${jobId}:`, err);
|
||||
}
|
||||
|
||||
// Validate and repair the resume JSON before PDF generation
|
||||
const validationResult = await validateAndRepairJson(baseResume, resumeDataSchema, `pdf-${jobId}`);
|
||||
if (!validationResult.success) {
|
||||
console.error(`❌ [Job ${jobId}] Resume validation failed: ${validationResult.error}`);
|
||||
return { success: false, error: `Resume validation failed: ${validationResult.error}` };
|
||||
}
|
||||
|
||||
if (validationResult.repaired) {
|
||||
console.log(`🔧 [Job ${jobId}] Resume JSON was repaired by AI`);
|
||||
}
|
||||
|
||||
const validatedResume = validationResult.data;
|
||||
|
||||
// Write modified resume to temp file
|
||||
const tempResumePath = join(RESUME_GEN_DIR, `temp_resume_${jobId}.json`);
|
||||
await writeFile(tempResumePath, JSON.stringify(baseResume, null, 2));
|
||||
await writeFile(tempResumePath, JSON.stringify(validatedResume, null, 2));
|
||||
|
||||
// Generate PDF using Python script - output directly to our data folder
|
||||
const outputFilename = `resume_${jobId}.pdf`;
|
||||
|
||||
151
orchestrator/src/shared/rxresume-schema.test.ts
Normal file
151
orchestrator/src/shared/rxresume-schema.test.ts
Normal file
@ -0,0 +1,151 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { idSchema, skillSchema, resumeDataSchema } from './rxresume-schema.js';
|
||||
|
||||
describe('RxResume Schema Validation', () => {
|
||||
describe('idSchema (CUID2)', () => {
|
||||
it('should accept valid CUID2 IDs', () => {
|
||||
const validIds = [
|
||||
'ck9w4ygzq0000xmn5h0jt7l5c',
|
||||
'clh2h3j4k0000abcd1234efgh',
|
||||
'abc123def456ghi789jkl012m',
|
||||
];
|
||||
|
||||
validIds.forEach(id => {
|
||||
const result = idSchema.safeParse(id);
|
||||
expect(result.success, `ID "${id}" should be valid`).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject invalid IDs like "skill-0"', () => {
|
||||
const invalidIds = [
|
||||
'skill-0',
|
||||
'skill-1',
|
||||
'skill-123',
|
||||
'item_1',
|
||||
'123abc', // starts with number
|
||||
'ABC123', // uppercase
|
||||
'', // empty
|
||||
];
|
||||
|
||||
invalidIds.forEach(id => {
|
||||
const result = idSchema.safeParse(id);
|
||||
expect(result.success, `ID "${id}" should be invalid`).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('skillSchema', () => {
|
||||
it('should accept valid skill with CUID2 ID', () => {
|
||||
const validSkill = {
|
||||
id: 'ck9w4ygzq0000xmn5h0jt7l5c',
|
||||
visible: true,
|
||||
name: 'JavaScript',
|
||||
description: '',
|
||||
level: 3,
|
||||
keywords: ['ES6', 'TypeScript'],
|
||||
};
|
||||
|
||||
const result = skillSchema.safeParse(validSkill);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject skill with invalid ID format', () => {
|
||||
const invalidSkill = {
|
||||
id: 'skill-0', // Invalid CUID2
|
||||
visible: true,
|
||||
name: 'JavaScript',
|
||||
description: '',
|
||||
level: 3,
|
||||
keywords: ['ES6'],
|
||||
};
|
||||
|
||||
const result = skillSchema.safeParse(invalidSkill);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues[0].path).toContain('id');
|
||||
expect(result.error.issues[0].message).toContain('cuid2');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('resumeDataSchema', () => {
|
||||
it('should reject resume with invalid skill IDs', () => {
|
||||
const resumeWithInvalidIds = {
|
||||
basics: {
|
||||
name: 'John Doe',
|
||||
headline: 'Developer',
|
||||
email: 'john@example.com',
|
||||
phone: '',
|
||||
location: '',
|
||||
url: { label: '', href: '' },
|
||||
customFields: [],
|
||||
picture: {
|
||||
url: '',
|
||||
size: 64,
|
||||
aspectRatio: 1,
|
||||
borderRadius: 0,
|
||||
effects: { hidden: false, border: false, grayscale: false },
|
||||
},
|
||||
},
|
||||
sections: {
|
||||
summary: { id: 'summary', name: 'Summary', columns: 1, separateLinks: true, visible: true, content: '' },
|
||||
skills: {
|
||||
id: 'skills',
|
||||
name: 'Skills',
|
||||
columns: 1,
|
||||
separateLinks: true,
|
||||
visible: true,
|
||||
items: [
|
||||
{
|
||||
id: 'skill-0', // Invalid!
|
||||
visible: true,
|
||||
name: 'JavaScript',
|
||||
description: '',
|
||||
level: 1,
|
||||
keywords: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
// Minimal required sections
|
||||
awards: { id: 'awards', name: 'Awards', columns: 1, separateLinks: true, visible: true, items: [] },
|
||||
certifications: { id: 'certifications', name: 'Certifications', columns: 1, separateLinks: true, visible: true, items: [] },
|
||||
education: { id: 'education', name: 'Education', columns: 1, separateLinks: true, visible: true, items: [] },
|
||||
experience: { id: 'experience', name: 'Experience', columns: 1, separateLinks: true, visible: true, items: [] },
|
||||
volunteer: { id: 'volunteer', name: 'Volunteer', columns: 1, separateLinks: true, visible: true, items: [] },
|
||||
interests: { id: 'interests', name: 'Interests', columns: 1, separateLinks: true, visible: true, items: [] },
|
||||
languages: { id: 'languages', name: 'Languages', columns: 1, separateLinks: true, visible: true, items: [] },
|
||||
profiles: { id: 'profiles', name: 'Profiles', columns: 1, separateLinks: true, visible: true, items: [] },
|
||||
projects: { id: 'projects', name: 'Projects', columns: 1, separateLinks: true, visible: true, items: [] },
|
||||
publications: { id: 'publications', name: 'Publications', columns: 1, separateLinks: true, visible: true, items: [] },
|
||||
references: { id: 'references', name: 'References', columns: 1, separateLinks: true, visible: true, items: [] },
|
||||
custom: {},
|
||||
},
|
||||
metadata: {
|
||||
template: 'rhyhorn',
|
||||
layout: [[['summary'], ['skills']]],
|
||||
css: { value: '', visible: false },
|
||||
page: { margin: 18, format: 'a4', options: { breakLine: true, pageNumbers: true } },
|
||||
theme: { background: '#ffffff', text: '#000000', primary: '#dc2626' },
|
||||
typography: {
|
||||
font: { family: 'IBM Plex Serif', subset: 'latin', variants: ['regular'], size: 14 },
|
||||
lineHeight: 1.5,
|
||||
hideIcons: false,
|
||||
underlineLinks: true,
|
||||
},
|
||||
notes: '',
|
||||
},
|
||||
};
|
||||
|
||||
const result = resumeDataSchema.safeParse(resumeWithInvalidIds);
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
if (!result.success) {
|
||||
// Should have error about the skill ID
|
||||
const idError = result.error.issues.find(
|
||||
issue => issue.path.join('.').includes('skills.items') && issue.path.includes('id')
|
||||
);
|
||||
expect(idError).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -10,7 +10,8 @@ export type FilterKeys<T, Condition> = {
|
||||
|
||||
export const idSchema = z
|
||||
.string()
|
||||
.describe("Unique identifier for the item");
|
||||
.cuid2()
|
||||
.describe("Unique identifier for the item (CUID2 format)");
|
||||
|
||||
export const itemSchema = z.object({
|
||||
id: idSchema,
|
||||
|
||||
@ -73,6 +73,8 @@ def _call_openrouter(prompt: str, model: str, api_key: str) -> str:
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"stream": False,
|
||||
"plugins": [{"id": "response-healing"}],
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
|
||||
@ -38,9 +38,72 @@ def login(page):
|
||||
|
||||
def import_resume(page, json_path: Path):
|
||||
"""Import a resume JSON file."""
|
||||
# Log the JSON file size for debugging
|
||||
try:
|
||||
import json
|
||||
with open(json_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
print(f" 📋 JSON keys: {list(data.keys())}")
|
||||
if 'basics' in data:
|
||||
print(f" 📋 Headline: {data['basics'].get('headline', 'N/A')[:50]}...")
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Could not read JSON for logging: {e}")
|
||||
|
||||
page.click('h4:has-text("Import")')
|
||||
page.set_input_files('input[type="file"]', str(json_path))
|
||||
page.click('button:has-text("Validate")')
|
||||
|
||||
# Wait for validation to complete - check for either success (Import button) or error
|
||||
try:
|
||||
# Wait for the Import button to become visible (validation succeeded)
|
||||
page.wait_for_selector('button:has-text("Import"):not([disabled])', timeout=10000)
|
||||
except Exception as e:
|
||||
# Save debug files to errors folder (accessible outside Docker)
|
||||
errors_dir = OUTPUT_DIR.parent / "errors"
|
||||
errors_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Take a screenshot for debugging
|
||||
try:
|
||||
screenshot_path = errors_dir / f"debug_{json_path.stem}.png"
|
||||
page.screenshot(path=str(screenshot_path))
|
||||
print(f" 📸 Debug screenshot saved: {screenshot_path}")
|
||||
except Exception as screenshot_err:
|
||||
print(f" ⚠️ Could not save screenshot: {screenshot_err}")
|
||||
|
||||
# Copy the failed JSON to errors folder for inspection
|
||||
try:
|
||||
import shutil
|
||||
failed_json_path = errors_dir / f"{json_path.stem}.json"
|
||||
shutil.copy(str(json_path), str(failed_json_path))
|
||||
print(f" 📋 Failed JSON saved: {failed_json_path}")
|
||||
except Exception as copy_err:
|
||||
print(f" ⚠️ Could not save failed JSON: {copy_err}")
|
||||
|
||||
# Check for validation error messages in the dialog
|
||||
error_selectors = [
|
||||
'text=/error|invalid|failed/i',
|
||||
'[class*="error"]',
|
||||
'[class*="destructive"]',
|
||||
'.text-red-500',
|
||||
'.text-destructive',
|
||||
'[role="alert"]',
|
||||
]
|
||||
for selector in error_selectors:
|
||||
error_element = page.query_selector(selector)
|
||||
if error_element:
|
||||
error_text = error_element.inner_text().strip()
|
||||
if error_text:
|
||||
print(f" ❌ RXResume validation error: {error_text}")
|
||||
raise RuntimeError(f"RXResume validation failed: {error_text}")
|
||||
|
||||
# Log what's visible in the dialog for debugging
|
||||
dialog = page.query_selector('[role="dialog"]')
|
||||
if dialog:
|
||||
dialog_text = dialog.inner_text()[:500]
|
||||
print(f" 📋 Dialog content: {dialog_text}")
|
||||
|
||||
raise RuntimeError(f"Import button not found after validation (timeout): {e}")
|
||||
|
||||
page.click('button:has-text("Import")')
|
||||
|
||||
|
||||
|
||||
@ -1,661 +0,0 @@
|
||||
{
|
||||
"basics": {
|
||||
"name": "Shaheer Sarfaraz",
|
||||
"headline": "Frontend Software Engineer (React/TypeScript) · Autodesk Intern · Open Source & Product Work",
|
||||
"email": "shaheer30sarfaraz@gmail.com",
|
||||
"phone": "+44 7359 501592",
|
||||
"location": "Blackpool, United Kingdom",
|
||||
"url": {
|
||||
"label": "https://dakheera47.com/",
|
||||
"href": "https://dakheera47.com/"
|
||||
},
|
||||
"customFields": [],
|
||||
"picture": {
|
||||
"url": "",
|
||||
"size": 120,
|
||||
"aspectRatio": 1,
|
||||
"borderRadius": 0,
|
||||
"effects": {
|
||||
"hidden": false,
|
||||
"border": false,
|
||||
"grayscale": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"sections": {
|
||||
"summary": {
|
||||
"name": "Summary",
|
||||
"columns": 1,
|
||||
"separateLinks": true,
|
||||
"visible": true,
|
||||
"id": "summary",
|
||||
"content": ""
|
||||
},
|
||||
"awards": {
|
||||
"name": "Awards",
|
||||
"columns": 1,
|
||||
"separateLinks": true,
|
||||
"visible": true,
|
||||
"id": "awards",
|
||||
"items": []
|
||||
},
|
||||
"certifications": {
|
||||
"name": "Certifications",
|
||||
"columns": 1,
|
||||
"separateLinks": true,
|
||||
"visible": true,
|
||||
"id": "certifications",
|
||||
"items": []
|
||||
},
|
||||
"education": {
|
||||
"name": "Education",
|
||||
"columns": 1,
|
||||
"separateLinks": true,
|
||||
"visible": true,
|
||||
"id": "education",
|
||||
"items": [
|
||||
{
|
||||
"id": "yo3p200zo45c6cdqc6a2vtt3",
|
||||
"visible": true,
|
||||
"institution": "University of Lancashire",
|
||||
"studyType": "BSc (Hons) Computer Science",
|
||||
"area": "Preston, United Kingdom",
|
||||
"score": "1st Class",
|
||||
"date": "September 2022 to June 2026",
|
||||
"summary": "<p style=\"text-align: left;\">Relevant Modules: Web Applications, Algorithms & Data Structures, Game Development, Databases, Software Engineering (Agile group project)</p>",
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": "https://www.lancashire.ac.uk/undergraduate/courses/computer-science-bsc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ei2fvjokusg3cfmdyolmgcoz",
|
||||
"visible": false,
|
||||
"institution": " ",
|
||||
"studyType": "",
|
||||
"area": "A Levels",
|
||||
"score": "",
|
||||
"date": "",
|
||||
"summary": "<ul><li><p>Maths: A</p></li><li><p>Computer Science: B</p></li><li><p>Physics: C</p></li><li><p>Chemistry: E</p></li></ul>",
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "pm4r5hngvv1w4mc79o22irfx",
|
||||
"visible": false,
|
||||
"institution": " ",
|
||||
"studyType": "",
|
||||
"area": "GCSEs",
|
||||
"score": "",
|
||||
"date": "",
|
||||
"summary": "<ol><li><p>English: A*</p></li><li><p>Computer Science: A*</p></li><li><p>Urdu: A</p></li><li><p>Islamiat: A</p></li><li><p>Pakistan Studies: A</p></li><li><p>Biology: A</p></li><li><p>Chemistry: A</p></li><li><p>Physics: A</p></li><li><p>Maths: A</p></li></ol>",
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"experience": {
|
||||
"name": "Experience",
|
||||
"columns": 1,
|
||||
"separateLinks": true,
|
||||
"visible": true,
|
||||
"id": "experience",
|
||||
"items": [
|
||||
{
|
||||
"id": "ng9ui2azk7w4y8oyu8kazqeb",
|
||||
"visible": true,
|
||||
"company": "Autodesk",
|
||||
"position": "Software Engineering Intern",
|
||||
"location": "Hybrid (Sheffield Based)",
|
||||
"date": "July 2024 - June 2025",
|
||||
"summary": "<ul><li><p><strong>Implemented front-end features and fixes</strong> in the Autodesk Construction Cloud Model Coordination app, working in a ~10-year-old React/JavaScript/TypeScript codebase (7k+ commits) using Webpack module federation and Autodesk’s Exoskeleton dev environment</p></li><li><p>Improved reliability of the <strong>Cypress end-to-end test suite</strong> by diagnosing flaky tests, adding new E2E coverage, and participating in focused “test fest” events ahead of major feature releases</p></li><li><p>Collaborated with cross-functional teams (like the Design System, platform teams) by <strong>raising well-scoped bugs</strong>, augmenting existing tickets with reproduction steps and context, and aligning on shared component and API changes</p></li><li><p>Helped strengthen team processes by <strong>running weekly stand-ups</strong> and retrospectives, organising a ticket-scoping meeting, and <strong>participating in technical reviews & ADR discussions</strong> (e.g. standardising error handling and planning clash data streaming)</p></li></ul>",
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "lhw25d7gf32wgdfpsktf6e0x",
|
||||
"visible": true,
|
||||
"company": "Mirage",
|
||||
"position": "Co-Founder & Lead Developer",
|
||||
"location": "",
|
||||
"date": "December 2019 to Present",
|
||||
"summary": "<ul><li><p>Delivered <strong>10+ production websites and webapps</strong> for small and medium size clients (e.g. Indus Marine Services, Mumtaz Urdu), from initial scoping to deployment and handover</p></li><li><p>Built with <strong>modern web stacks</strong> (Next.js, Node/Express, Tailwind, Strapi, WordPress/Elementor where appropriate), setting up CI/CD and hosting</p></li><li><p><strong>Led a small team of four developers</strong>, handling code reviews, task breakdown, and client communication</p></li></ul>",
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": "https://promirage.com/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "k6zxqunkb225hbjso3c3vykk",
|
||||
"visible": true,
|
||||
"company": "University of Lancashire",
|
||||
"position": "Computing Student Mentor",
|
||||
"location": "Preston, UK",
|
||||
"date": "July 2023 - July 2024",
|
||||
"summary": "<ul><li><p><strong>Academic Support and Leadership:</strong> Provided academic guidance to over 10 first-year students once a week, significantly enhancing their understanding and skills in key subjects like programming and web development.</p></li><li><p style=\"text-align: start\"><strong>Collaborative Learning Environment:</strong> Actively fostered a collaborative and supportive learning environment for a group of 10 students. This role also honed my leadership and communication skills, facilitating better academic outcomes for mentees.</p></li></ul>",
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "a1bg5d8gp8sulf91xzdcsiaq",
|
||||
"visible": true,
|
||||
"company": "Research and Knowledge Exchange Institute",
|
||||
"position": "Undergraduate Research Intern (HCI & EdTech)",
|
||||
"location": "",
|
||||
"date": "Summer 2024",
|
||||
"summary": "<ul><li><p>Built a <strong>mouse “torch-reveal” web app</strong> (<strong>Astro</strong>) to approximate eye-tracking; ran on-campus studies with Revoe Learning Academy pupils—<strong>1</strong> eye-tracked, <strong>9</strong> using my app.</p></li><li><p>Logged cursor paths, dwell time, and reveal order; delivered setup notes for staff to run sessions independently.</p></li><li><p>Developed a <strong>Questionnaire Randomiser</strong> (Next.js): selectable response metrics (<strong>smileys / numbers / stars</strong>), configurable randomisation strategies, and <strong>ZIP export of per-student PDFs</strong> ready for print.</p></li><li><p>Extras: lightweight analytics for comparison with the eye-tracking baseline; optional CSV/JSON data export.</p></li></ul>",
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "tx32suzrg2bs5eumcbjei4ns",
|
||||
"visible": false,
|
||||
"company": "University of Lancashire",
|
||||
"position": "Student Ambassador",
|
||||
"location": "Preston, UK",
|
||||
"date": "July 2023 - Present",
|
||||
"summary": "<ul><li><p><strong>Diverse Role Engagement:</strong> Actively engaged in various tasks, from guiding tours to assisting on open days, demonstrating adaptability and organizational skills.</p></li><li><p><strong>Campus Culture Promotion:</strong> Contributed to enhancing the university’s inclusive campus atmosphere, showcasing the university's vibrant community to prospective students.</p></li></ul>",
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"volunteer": {
|
||||
"name": "Volunteering",
|
||||
"columns": 1,
|
||||
"separateLinks": true,
|
||||
"visible": true,
|
||||
"id": "volunteer",
|
||||
"items": []
|
||||
},
|
||||
"interests": {
|
||||
"name": "Interests",
|
||||
"columns": 1,
|
||||
"separateLinks": true,
|
||||
"visible": false,
|
||||
"id": "interests",
|
||||
"items": []
|
||||
},
|
||||
"languages": {
|
||||
"name": "Languages",
|
||||
"columns": 1,
|
||||
"separateLinks": true,
|
||||
"visible": true,
|
||||
"id": "languages",
|
||||
"items": []
|
||||
},
|
||||
"profiles": {
|
||||
"name": "Profiles",
|
||||
"columns": 1,
|
||||
"separateLinks": true,
|
||||
"visible": true,
|
||||
"id": "profiles",
|
||||
"items": [
|
||||
{
|
||||
"id": "ukl0uecvzkgm27mlye0wazlb",
|
||||
"visible": true,
|
||||
"network": "GitHub",
|
||||
"username": "DaKheera47",
|
||||
"icon": "github",
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": "https://github.com/DaKheera47"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cnbk5f0aeqvhx69ebk7hktwd",
|
||||
"visible": true,
|
||||
"network": "LinkedIn",
|
||||
"username": "ssarfaraz30",
|
||||
"icon": "linkedin",
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": "https://www.linkedin.com/in/ssarfaraz30/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "linnyxv78zdep1xwirpa2ia1",
|
||||
"visible": true,
|
||||
"network": "Hashnode",
|
||||
"username": "DaKheera47",
|
||||
"icon": "hashnode",
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": "https://dakheera47.hashnode.dev/"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"projects": {
|
||||
"name": "Projects",
|
||||
"columns": 1,
|
||||
"separateLinks": true,
|
||||
"visible": true,
|
||||
"id": "projects",
|
||||
"items": [
|
||||
{
|
||||
"id": "yw843emozcth8s1ubi1ubvlf",
|
||||
"visible": false,
|
||||
"name": "Atoro",
|
||||
"description": "Lead Developer",
|
||||
"date": "January 2023",
|
||||
"summary": "<ol><li><p><strong>Next.js Implementation for Enhanced SEO:</strong> Utilized Next.js to optimize the website for search engines, significantly improving its online visibility and user engagement.</p></li><li><p><strong>Strapi Backend Integration:</strong> Streamlined content management by implementing a Strapi backend, enhancing the efficiency and scalability of the website's content updates.</p></li><li><p><strong>Responsive Design with Tailwind CSS:</strong> Employed Tailwind CSS for a utility-first approach, ensuring the website's responsiveness and seamless user experience across various devices.</p></li><li><p><strong>Continuous Deployment Pipeline Establishment:</strong> Developed a continuous deployment pipeline, ensuring real-time updates and maintaining high performance and reliability of the website.</p></li><li><p><strong>Optimized Web Performance:</strong> Focused on optimizing web performance by efficiently loading images and managing JavaScript bundles, leading to a faster and more efficient user experience.</p></li></ol>",
|
||||
"keywords": [],
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": "https://atoro.promirage.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ncxgdjjky54gh59iz2t1xi1v",
|
||||
"visible": false,
|
||||
"name": "Stellar Consultancy",
|
||||
"description": "Lead Developer",
|
||||
"date": "April 2023",
|
||||
"summary": "<ol><li><p><strong>WordPress and Elementor Integration:</strong> Expertly utilized WordPress with Elementor to build a robust content management system, enhancing the website's scalability and user interaction capabilities.</p></li><li><p><strong>Client Engagement and Trust Building:</strong> Implemented features to showcase client testimonials, effectively building trust and displaying the success of previous project engagements.</p></li><li><p><strong>Intuitive Design and User Engagement:</strong> Focused on intuitive page design and structuring, streamlining site maintenance and content updates, thereby enhancing user engagement.</p></li><li><p><strong>Effective Call-to-Actions:</strong> Crafted clear call-to-actions and provided essential contact information, significantly improving user interaction and conversion rates.</p></li><li><p><strong>Portfolio Display for Business Showcase:</strong> Presented past work and services offered through a comprehensive portfolio display, allowing visitors to assess the quality and impact of Stellar Consultancy's services.</p></li></ol>",
|
||||
"keywords": [],
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": "https://stellarconsultancy.ca"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "tcecguinuctb8mu2xqrn97m8",
|
||||
"visible": true,
|
||||
"name": "Mumtaz Urdu",
|
||||
"description": "Developer",
|
||||
"date": "July 2022",
|
||||
"summary": "<ol><li><p><strong>Server-Rendered Web Application Development</strong>: Created the Mumtaz Urdu platform with Next.js to optimize server-side rendering for enhanced SEO and performance.</p></li><li><p><strong>UI Development with Tailwind CSS</strong>: Implemented utility-first Tailwind CSS, ensuring rapid, responsive design for a seamless user interface.</p></li><li><p><strong>Scalable Storage Solution</strong>: Integrated scalable Amazon S3 storage, supporting the application's growth and robust data management.</p></li><li><p><strong>Progressive Web App Implementation</strong>: Developed PWA features for Mumtaz Urdu, offering users native-like mobile access and increased engagement.</p></li><li><p><strong>High Traffic Data Management</strong>: Engineered Mumtaz Urdu's backend with Next.js and MongoDB, enabling the handling and efficient processing of vast amounts of user data for thousands of monthly users.</p></li><li><p><strong>Test-Driven Development</strong>: Embraced TDD practices to ensure reliable and high-quality code, facilitating regular testing throughout the development process for continuous improvement.</p></li></ol>",
|
||||
"keywords": [],
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": "https://www.mumtazurdu.com/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "to47h749kaj6t02j3f9kprxq",
|
||||
"visible": false,
|
||||
"name": "PyScreeze",
|
||||
"description": "Open Source Contribution",
|
||||
"date": "January 2022",
|
||||
"summary": "<ol><li><p><strong>Innovative Feature Implementation:</strong> Implemented the <code>locateCenterOnScreenNear</code> function for <code>PyScreeze</code>, enhancing the library's functionality by enabling precise image location near a specified point on the screen.</p></li><li><p><strong>Open Source Contribution:</strong> Marked my debut in open-source contributions with this significant addition to <code>PyScreeze</code>, showcasing my initiative and ability to contribute effectively to community-driven projects.</p></li><li><p><strong>Collaborative Development and Recognition:</strong> Collaborated with the project's maintainer, <code>asweigart</code>, to refine and integrate the function into the main codebase, receiving recognition for this valuable contribution to the project.</p></li></ol>",
|
||||
"keywords": [],
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": "https://github.com/asweigart/pyscreeze/pull/79"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "gt7yq82ulor5hmmutdhuvfo1",
|
||||
"visible": false,
|
||||
"name": "Threegency",
|
||||
"description": "Lead Developer",
|
||||
"date": "February 2023",
|
||||
"summary": "<ul><li><p><strong>Framework</strong>: Utilized Next.js to build a server-rendered React website, enhancing SEO and ensuring optimal performance.</p></li><li><p><strong>Styling</strong>: Employed Tailwind CSS for utility-first styling, facilitating rapid UI development.</p></li><li><p><strong>Content Management</strong>: Leveraged Strapi as a CMS, enabling streamlined content updates and administration.</p></li><li><p><strong>Data Handling</strong>: Utilized GraphQL for data handling, ensuring efficient and flexible data retrieval.</p></li></ul>",
|
||||
"keywords": [],
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": "https://www.threegency.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "c8fcu3nz541a4d5zcurx6b8c",
|
||||
"visible": false,
|
||||
"name": "AutoClass",
|
||||
"description": "GUI Automation",
|
||||
"date": "November 2021",
|
||||
"summary": "<ul><li><p><strong>Framework</strong>: Written in Python, leveraging the versatility and ease-of-use of the language.</p></li><li><p><strong>Automation Library</strong>: Utilized PyAutoGUI for automating user interactions, enhancing the utility of the application.</p></li><li><p><strong>Iterative Improvement</strong>: Progressively refined over a year, demonstrating a commitment to robustness and reliability.</p></li><li><p><strong>Project Purpose</strong>: Developed to automate the process of joining Zoom classes, alleviating the repetitive morning routine.</p></li></ul>",
|
||||
"keywords": [],
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": "https://github.com/DaKheera47/autoclass"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rv23bgibq6bye6rujmcx1ygc",
|
||||
"visible": false,
|
||||
"name": "Meet Link Generator",
|
||||
"description": "GUI Automation",
|
||||
"date": "January 2022",
|
||||
"summary": "<ul><li><p><strong>Functionality</strong>: Generates Google Meet links with specific words in the URL by brute-forcing the creation of thousands of links until the desired pattern is achieved. Doing so enables creation of Google Meet links with specific codes or phrases.</p></li><li><p><strong>Optimized Automation</strong>: The final product uses Python with PyAutoGUI for efficient and rapid creation of new Google Meet links.</p></li><li><p><strong>Speed and Efficiency</strong>: Drastically improved performance, finally achieving the link generation time to under 1 second per link, limited only by internet speed.</p></li><li><p><strong>Interface Interaction</strong>: Utilizes the Google Meet homepage's features for quicker link generation, avoiding full page refreshes for speed.</p></li></ul>",
|
||||
"keywords": [],
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": "https://github.com/DaKheera47/meet-link-generator"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "tu98rghbi5c43ogget5mh7ih",
|
||||
"visible": false,
|
||||
"name": "UCLan Server-side Web Application Project",
|
||||
"description": "",
|
||||
"date": "UCLan Year 1",
|
||||
"summary": "<ul><li><p><strong>Backend Development with PHP and MySQL:</strong> Developed the backend for a Student’s Union Shop web application, integrating PHP and MySQL for dynamic data handling and backend database communication.</p></li><li><p><strong>User Authentication and Session Management:</strong> Implemented user sign-up and login functionality using PHP sessions, enabling secure and personalized shopping experiences.</p></li><li><p><strong>Dynamic Content Display from Database:</strong> Enhanced the application to dynamically display products and offers directly from the database, moving away from static HTML content.</p></li><li><p><strong>Advanced Search and Personalization Features:</strong> Integrated advanced product search capabilities and personalized user greetings, improving user interactivity and engagement.</p></li></ul>",
|
||||
"keywords": [],
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ov4lkbc1vl169ynfnj91m1lm",
|
||||
"visible": false,
|
||||
"name": "Square About",
|
||||
"description": "",
|
||||
"date": "UCLan Year 1",
|
||||
"summary": "<ul><li><p><strong>Advanced 3D Game Development:</strong> Implemented a complex 3D game using TL-Engine, featuring intricate gameplay mechanics and immersive 3D visuals.</p></li><li><p><strong>Dynamic Gameplay Elements:</strong> Integrated multiple spheres with varying behaviors, including super-spheres requiring multiple hits, enhancing the game's challenge and engagement levels.</p></li><li><p><strong>Interactive Game Controls:</strong> Developed features for speed control and directional change, allowing players to interact dynamically with the game environment.</p></li><li><p><strong>Strategic Game Mechanics:</strong> Added a bullet firing mechanism with a limited ammo concept, introducing strategic elements and a scoring system to the gameplay.</p></li></ul>",
|
||||
"keywords": [],
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "s3r37gdr0oa84a6dp6r5nl58",
|
||||
"visible": false,
|
||||
"name": "Car Smash",
|
||||
"description": "",
|
||||
"date": "UCLan Year 1",
|
||||
"summary": "<ol><li><p><strong>3D Car Smash Game Development:</strong> Developed a 3D car smash game using TL-Engine, showcasing skills in game engine utilization and 3D gaming.</p></li><li><p><strong>Collision Detection Mechanics:</strong> Implemented advanced collision detection between player's car and enemy vehicles, enhancing gameplay realism.</p></li><li><p><strong>Dynamic Game States and Camera Views:</strong> Integrated multiple game states and camera views, including a chase camera and first-person view, for an immersive gaming experience.</p></li><li><p><strong>Enhanced Player Interaction:</strong> Created a more realistic driving experience with accelerated movement and bounce effects on collisions, and introduced particle systems for visual effects.</p></li></ol>",
|
||||
"keywords": [],
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "gylzkvl103m9s7ywag4xpdy4",
|
||||
"visible": false,
|
||||
"name": "Tweet Filter",
|
||||
"description": "",
|
||||
"date": "UCLan Year 1",
|
||||
"summary": "<ol><li><p><strong>Tweet Filtration System:</strong> Crafted a C++ program to filter out prohibited words from tweets, showcasing text processing and file handling capabilities.</p></li><li><p><strong>Advanced Text Manipulation:</strong> Enhanced the program to filter varying cases and contexts of banned words, even within larger strings, demonstrating attention to detail in string operations.</p></li><li><p><strong>Output Generation:</strong> Implemented functionality to write filtered tweets to new files, maintaining data integrity and displaying proficiency in file I/O operations.</p></li><li><p><strong>Algorithm Optimization:</strong> Utilized data structures like vectors and implemented mathematical techniques for efficient word frequency analysis and sentiment determination.</p></li></ol>",
|
||||
"keywords": [],
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "enav754zxhuc9uycbb83s94q",
|
||||
"visible": false,
|
||||
"name": "Burger Ordering App",
|
||||
"description": "",
|
||||
"date": "UCLan Year 1",
|
||||
"summary": "<ol><li><p><strong>Interactive Console Application:</strong> Engineered a C++ console application simulating a burger ordering process, highlighting proficiency in creating user-interactive software.</p></li><li><p><strong>Complex Logic Implementation:</strong> Designed and implemented complex logic for burger size and topping selection, including pricing and order summary features.</p></li><li><p><strong>Data Handling and User Input:</strong> Developed robust credit system and user input validation for an intuitive ordering experience, showcasing attention to detail and user-centric design.</p></li><li><p><strong>Readable and Maintainable Code:</strong> Produced well-documented, maintainable code with clear variable naming and structured formatting, demonstrating best practices in software development.</p></li></ol>",
|
||||
"keywords": [],
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "hl6jgeswr01tlul3iwoat05d",
|
||||
"visible": false,
|
||||
"name": "LinkLander",
|
||||
"description": "Android Studio, Kotlin",
|
||||
"date": "December 2023 - Ongoing",
|
||||
"summary": "<ul><li><p><strong>Innovative Android Utility:</strong> Developed LinkLander, a Kotlin-based Android application that simplifies the process of downloading online content directly to devices.</p></li><li><p><strong>User-Centric Design:</strong> Focused on addressing Android system limitations by providing a seamless shortcut for redirecting links to an online video downloading service.</p></li><li><p><strong>Simplicity and Efficiency:</strong> Emphasized a user-friendly interface, enhancing the Android experience by streamlining content downloads.</p></li><li><p><strong>Technical Proficiency in Kotlin:</strong> Leveraged the capabilities of Kotlin for Android development to create a practical solution for niche digital tasks.</p></li></ul>",
|
||||
"keywords": [],
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "v4s0ljbiiio198y8l1wl0ym6",
|
||||
"visible": false,
|
||||
"name": "AR App Development with AGILE",
|
||||
"description": "Unity, C#",
|
||||
"date": "October 2023 - Ongoing",
|
||||
"summary": "<ul><li><p><strong>Agile Development in Action</strong>: Participated in an Agile team project, developing an AR application for supporting disabled students with a team of five, demonstrating an application of Agile methodologies in a real-world scenario.</p></li><li><p><strong>Mobile AR Application Prototype</strong>: Developed a proof-of-concept prototype using Unity and C# for mobile platforms, showcasing technical skills in modern app development environments.</p></li><li><p><strong>Collaborative Software Engineering</strong>: Engaged in a collaborative environment, contributing code and ideas, emphasizing teamwork and shared responsibility in software creation.</p></li><li><p><strong>Presentation and Critical Analysis</strong>: Delivered a comprehensive presentation and critical report, evaluating the Agile process, product development, and personal learning outcomes.</p></li></ul>",
|
||||
"keywords": [],
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fwxrq682hqrj1y76rmziqrbk",
|
||||
"visible": true,
|
||||
"name": "Indus Marine Services",
|
||||
"description": "System Design & Development",
|
||||
"date": "May 2022 - Ongoing",
|
||||
"summary": "<ol><li><p><strong>Induction System for Marine Services</strong>: Designed & developed an induction system for Indus Marine Services in the UAE, streamlining the employee onboarding process with interactive testing and certification issuance.</p></li><li><p><strong>Admin-Centric Functionality</strong>: Devised a back-end system allowing admins to oversee inductee progress, manage documents, and curate customized quizzes as per requirements</p></li><li><p><strong>Client Engagement Interface</strong>: Implemented a user-friendly front-end where inductees receive personalized email prompts, complete quizzes, and obtain certifications, all contributing to a seamless induction experience.</p></li><li><p><strong>Robust Tech Stack Integration</strong>: Utilized a sophisticated stack comprising Node.js, Express, EJS, and Tailwind CSS to build a responsive, scalable, and easily navigable system.</p></li></ol>",
|
||||
"keywords": [],
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": "http://www.ims-auh.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "jdfyaez8vq1b7xfr9rmxmz06",
|
||||
"visible": false,
|
||||
"name": "VECTOR AI",
|
||||
"description": "Website Development",
|
||||
"date": "February 2024 - February 2024",
|
||||
"summary": "<ol><li><p><strong>Innovative AI Development</strong>: As the driving force behind VECTOR's website development, I spearheaded the technical design using Astro, with a cutting-edge stack including React and Tailwind CSS.</p></li><li><p><strong>Data-Driven Content Strategy</strong>: Leveraged Astro content management capabilities to structure and present data, ensuring content is dynamic, easily accessible, and optimized for both performance and scalability.</p></li><li><p><strong>Astro for Enhanced Performance</strong>: Utilized Astro for static site generation, making VECTOR's website performance fast for a pleasant user experience</p></li><li><p><strong>React for Responsive Interaction</strong>: Utilized React’s robust ecosystem to develop interactive elements, ensuring that each module of VECTOR’s platform is engaging and seamless for users across various touchpoints.</p></li></ol>",
|
||||
"keywords": [],
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": "https://vector-ai.co/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "qdhmfkqpfql19ohfas1g91ek",
|
||||
"visible": false,
|
||||
"name": "UCLan's First Hackathon",
|
||||
"description": "Hackathon, Team Work",
|
||||
"date": "February 2024",
|
||||
"summary": "<ol><li><p><strong>Second Place in UCLan Hackathon</strong>: Earned second place in UCLan's first hackathon by developing an app to simplify university life. Focused on enhancing the attendance monitoring process for student mentors.</p></li><li><p><strong>TRPC for End-to-End Type Safety</strong>: Utilized TRPC to ensure end-to-end type safety, enhancing the app's reliability and streamlining the development process.</p></li><li><p><strong>Supabase Backend Integration</strong>: Implemented Supabase as a backend solution, providing a robust and scalable database for managing attendance data efficiently.</p></li><li><p><strong>Amazon SES and OAuth Integration</strong>: Integrated Amazon SES for email notifications and OAuth for secure Google login, improving user experience and communication.</p></li></ol>",
|
||||
"keywords": [],
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rw3x7tapntrt877rbl4pnxz7",
|
||||
"visible": true,
|
||||
"name": "NASA Space Apps Challenge",
|
||||
"description": "A 48-hour, global hackathon powered by NASA open data",
|
||||
"date": "Oct 4–5, 2025",
|
||||
"summary": "<ol><li><p><strong>Full-Stack Integration:</strong> Wired up backend services to a responsive frontend, enabling real-time exploration of <strong>Kepler/K2/TESS</strong> catalogs and smooth model-scoring UX.</p></li><li><p><strong>Data Harmonization Pipeline:</strong> Cleaned, merged, and standardized multi-mission catalogs into a unified schema, unblocking ML teammates and cutting data-prep time by <strong>60%+</strong> during the hack.</p></li><li><p><strong>Analytics UI & Upload Flow:</strong> Built an upload → validate → score workflow and a clear results dashboard so researchers can triage candidates in minutes, not hours.</p></li><li><p><strong>Delivery Under Pressure:</strong> Coordinated a <strong>5-person</strong> multidisciplinary team to ship a working web app in <strong>48 hours</strong>, with demo-ready reliability for judging.</p></li></ol>",
|
||||
"keywords": [],
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": "https://exploranium.vercel.app/dashboard"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "i2t6epmx5v7s0d8rqtxsigp3",
|
||||
"visible": true,
|
||||
"name": "Strong Statistics",
|
||||
"description": "Self-hosted strength analytics app using FastAPI and Next.js to visualize Strong app data with full local privacy and active open-source adoption.",
|
||||
"date": "September 2025 - Present",
|
||||
"summary": "<ol><li><p><strong>Self-Hosted Strength Analytics Platform:</strong> Developed <em>strong-statistics</em>, an open-source web app that visualizes detailed workout analytics from the <strong>Strong </strong>and<strong> Hevy</strong> fitness app, giving users local control of their training data.</p></li><li><p><strong>Full-Stack Architecture:</strong> Built a modular stack with <strong>FastAPI</strong>, <strong>Next.js</strong>, <strong>Tailwind CSS</strong>, and <strong>SQLite</strong>, deployed via <strong>Docker Compose</strong> for seamless self-hosting and persistent local data storage.</p></li><li><p><strong>Active Open-Source Ecosystem:</strong> Published on GitHub with <strong>community engagement from global users</strong> — external contributors opened feature requests and bug reports, validating real-world adoption and reliability.</p></li><li><p><strong>Continuous Personal Use & Maintenance:</strong> Regularly updated and used in live deployment at <a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"http://lifting.dakheera47.com\">lifting.dakheera47.com</a>, tracking <strong>hundreds of sets</strong> over time with persistent analytics and performance trends.</p></li></ol>",
|
||||
"keywords": [],
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": "https://lifting.dakheera47.com/"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"publications": {
|
||||
"name": "Publications",
|
||||
"columns": 1,
|
||||
"separateLinks": true,
|
||||
"visible": true,
|
||||
"id": "publications",
|
||||
"items": []
|
||||
},
|
||||
"references": {
|
||||
"name": "References",
|
||||
"columns": 1,
|
||||
"separateLinks": true,
|
||||
"visible": false,
|
||||
"id": "references",
|
||||
"items": [
|
||||
{
|
||||
"id": "f2sv5z0cce6ztjl87yuk8fak",
|
||||
"visible": true,
|
||||
"name": "Available upon request",
|
||||
"description": "",
|
||||
"summary": "",
|
||||
"url": {
|
||||
"label": "",
|
||||
"href": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"skills": {
|
||||
"name": "Skills",
|
||||
"columns": 2,
|
||||
"separateLinks": true,
|
||||
"visible": true,
|
||||
"id": "skills",
|
||||
"items": [
|
||||
{
|
||||
"id": "jfgzfcwcg65k9gemuxlfe9m3",
|
||||
"visible": true,
|
||||
"name": "Frontend Development",
|
||||
"description": "",
|
||||
"level": 0,
|
||||
"keywords": [
|
||||
"React",
|
||||
"Next.js",
|
||||
"Tailwind CSS",
|
||||
"Strapi CMS",
|
||||
"Elementor",
|
||||
"GraphQL",
|
||||
"TypeScript",
|
||||
"CI/CD",
|
||||
"PWA Development",
|
||||
"AstroJS",
|
||||
"React Testing Library"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sk3957foopxir2hw4xzxqahh",
|
||||
"visible": true,
|
||||
"name": "Backend Development",
|
||||
"description": "",
|
||||
"level": 0,
|
||||
"keywords": [
|
||||
"Node.js",
|
||||
"Express.js",
|
||||
"MongoDB",
|
||||
"Supabase",
|
||||
"Firebase",
|
||||
"Docker",
|
||||
"FastAPI",
|
||||
"AWS S3",
|
||||
"AWS SES"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "d9bddwdj6qreknhk644rm0bs",
|
||||
"visible": true,
|
||||
"name": "Leadership and Problem-Solving",
|
||||
"description": "",
|
||||
"level": 0,
|
||||
"keywords": [
|
||||
"Agile Project Management",
|
||||
"Conflict Resolution",
|
||||
"Creative Problem-Solving",
|
||||
"Decision-Making",
|
||||
"Effective Communication",
|
||||
"Adaptability"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "gk4hrky0wnbsbdcmmud48zjh",
|
||||
"visible": true,
|
||||
"name": "Other Programming",
|
||||
"description": "",
|
||||
"level": 0,
|
||||
"keywords": [
|
||||
"Python Scripting",
|
||||
"PyAutoGUI",
|
||||
"Git",
|
||||
"GitHub",
|
||||
"Selenium",
|
||||
"Data Analysis",
|
||||
"Web Scraping",
|
||||
"Data Cleaning"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"custom": {}
|
||||
},
|
||||
"metadata": {
|
||||
"template": "onyx",
|
||||
"layout": [
|
||||
[
|
||||
[
|
||||
"summary",
|
||||
"education",
|
||||
"experience",
|
||||
"projects",
|
||||
"references"
|
||||
],
|
||||
[
|
||||
"profiles",
|
||||
"skills",
|
||||
"certifications",
|
||||
"interests",
|
||||
"languages",
|
||||
"awards",
|
||||
"volunteer",
|
||||
"publications"
|
||||
]
|
||||
]
|
||||
],
|
||||
"css": {
|
||||
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"visible": false
|
||||
},
|
||||
"page": {
|
||||
"margin": 34,
|
||||
"format": "a4",
|
||||
"options": {
|
||||
"breakLine": false,
|
||||
"pageNumbers": false
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
"background": "#ffffff",
|
||||
"text": "#000000",
|
||||
"primary": "#475569"
|
||||
},
|
||||
"typography": {
|
||||
"font": {
|
||||
"family": "IBM Plex Sans",
|
||||
"subset": "latin",
|
||||
"variants": [
|
||||
"regular"
|
||||
],
|
||||
"size": 13
|
||||
},
|
||||
"lineHeight": 1.75,
|
||||
"hideIcons": false,
|
||||
"underlineLinks": true
|
||||
},
|
||||
"notes": ""
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user