From 38fb9e4330cfbf4074c4b5de1e927617bf30f87f Mon Sep 17 00:00:00 2001 From: LucaP Date: Fri, 4 Sep 2026 20:22:45 +0300 Subject: [PATCH] content: publish evidence-led portfolio copy --- content/public/portfolio-content.json | 187 ++++++++++++-------- docs/content-approval-checklist.md | 17 +- src/content/schemas.ts | 21 +++ src/content/types.ts | 13 ++ tests/fixtures/portfolio-content.valid.json | 16 +- tests/integration/public-content.test.ts | 21 ++- tests/unit/components.test.ts | 7 + tests/unit/content-schemas.test.ts | 39 +++- 8 files changed, 236 insertions(+), 85 deletions(-) diff --git a/content/public/portfolio-content.json b/content/public/portfolio-content.json index d85b725..e85b5ad 100644 --- a/content/public/portfolio-content.json +++ b/content/public/portfolio-content.json @@ -2,7 +2,9 @@ "identity": { "name": "Luca Pîslaru", "role": "System Administrator & Full-Stack Developer", - "valueProposition": "Building and operating reliable systems and software." + "valueProposition": "I build full-stack software and operate the Linux systems behind it, from offline-first applications to access-controlled self-hosted services and careful data migrations.", + "location": { "city": "Bucharest", "country": "Romania", "countryCode": "RO" }, + "availability": "Based in Bucharest and open to on-site, hybrid, or remote roles locally, as well as international remote opportunities." }, "navigation": [ { "id": "start", "label": "Start" }, @@ -15,81 +17,98 @@ { "id": "contact", "label": "Contact" } ], "profile": { - "introduction": "I work across Linux infrastructure and full-stack delivery, pairing practical operations with software built to be understood, maintained, and improved over time.", + "introduction": "I’m an early-career technologist working across Linux administration and end-to-end application development. I value systems that can be understood, maintained, secured, and recovered: clear access boundaries, deliberate deployments, useful documentation, and software refined through real use.", "screeningFacts": [ { "label": "Education", "value": "Bachelor's degree in Business Management IT, Romanian-American University, 2023-2026" }, { "label": "Languages", "value": "Romanian (native), English (C2), French (A1)" }, - { "label": "Certification", "value": "PCEP - Certified Entry-Level Python Programmer, 2022" } + { "label": "Certifications", "value": "Advanced Digital Competencies Certificate, 2025; PCEP - Certified Entry-Level Python Programmer, 2022" } ] }, "experience": [ { - "id": "mihai-razus-pfa", - "role": "Full-Stack Developer & DevOps collaborator", - "organization": "Mihai Razus PFA", - "dates": "Aug 2025 - Present", + "id": "trupa-lui-razus", + "role": "Full-Stack Developer & Hosting Administrator (Collaborator)", + "organization": "Trupa lui Răzuș", + "dates": "August 2025-Present", + "location": "Bucharest, Romania", "highlights": [ - "Contribute to full-stack delivery across application and deployment work.", - "Support practical DevOps collaboration alongside software development." + "Built the theatre group's first owned web presence, replacing a social-media-only discovery path with an indexable website and direct contact options.", + "Developed a custom Vue frontend and a headless WordPress publishing workflow that lets an editor publish news and promotional content without changing code.", + "Configured role-separated CMS access and restricted administrative routes and endpoints while keeping exact access rules private.", + "Manage the domain, cPanel hosting, DNS/TLS configuration, custom .htaccess behavior, and controlled Git/SSH deployments with verification and rollback." ] }, { "id": "camera-consultantilor-fiscali", "role": "Green Innovation Intern", "organization": "Camera Consultanților Fiscali", - "dates": "Jun 2025 - Aug 2025", + "dates": "June 2025-August 2025", + "location": "Bucharest, Romania", "highlights": [ - "Contributed to green innovation work in an internship setting.", - "Applied structured problem-solving and digital skills to assigned work." + "Contributed to eco-innovation work within the INOVATIV 2030 program.", + "Developed and presented sustainable-technology business models." ] }, { "id": "isense-solutions", "role": "Data Science Intern", "organization": "iSense Solutions", - "dates": "Jun 2025 - Aug 2025", + "dates": "June 2025-August 2025", + "location": "Bucharest, Romania", "highlights": [ - "Contributed to data science work in an internship setting.", - "Applied analytical and programming skills to assigned tasks." + "Processed and analyzed datasets using SPSS and Microsoft Excel.", + "Prepared quantitative reports supporting business-development decisions." ] } ], "systemsOperations": { "id": "professional-systems-operations", "title": "Professional systems operations", - "summary": "Professional work that connects application delivery with the Linux environments and deployment practices that support it.", + "summary": "Application delivery and hosting work with explicit boundaries between public content, authenticated administration, controlled releases, and recovery.", "evidence": [ - "Linux server administration and service management.", - "Docker and Nginx deployment work, including TLS-aware service delivery.", - "Operational troubleshooting alongside full-stack development." + "Public Vue frontend backed by a role-separated headless WordPress editing workflow.", + "Private source control followed by a deliberate SSH deployment, post-release verification, and Git-based rollback path.", + "Domain, DNS, TLS, cPanel, and restricted-route administration performed alongside application development." ] }, "infrastructure": { "id": "self-hosted-platform-lifecycle", "title": "Secure self-hosted platform and lifecycle", - "summary": "A personal Linux platform operated as a compact systems case study: secure ingress, deliberate access controls, service lifecycle discipline, and routine maintenance.", + "summary": "A self-hosted Linux environment operated as a long-term systems practice: traffic enters through a controlled edge, selected services receive an additional identity boundary, changes are staged deliberately, and important data has local and offsite recovery paths.", "bands": [ { "id": "ingress-and-tls", "title": "Ingress and TLS", - "summary": "Routes services through Nginx with certificate issuance and renewal managed through Let's Encrypt and Certbot; exposed entry points receive fail2ban protection." + "summary": "Route services through Nginx, manage DNS and certificate lifecycle, and use Fail2Ban at exposed entry points." }, { "id": "identity-and-access", "title": "Identity and access", - "summary": "Uses an authentication layer for protected services, with passkeys, two-factor authentication, and recovery considered without publishing policy details." + "summary": "Place selected services behind Authelia and constrain administrative, filesystem, and container access without publishing the underlying policy." }, { "id": "service-lifecycle", "title": "Service lifecycle", - "summary": "Deploys containerized services, manages updates, and treats service enrollment and retirement as part of ongoing maintenance." + "summary": "Deploy and update services with Docker Compose, including a full platform migration from a Raspberry Pi to a dedicated Linux server." }, { - "id": "operations-and-maintenance", - "title": "Operations", - "summary": "Investigates service issues and performs routine maintenance while keeping the platform understandable and manageable." - } - ] + "id": "operations-and-recovery", + "title": "Operations and recovery", + "summary": "Use Btrfs storage, a pre-upgrade backup script, rsync-based copies, and an offsite backup for important data." + } + ], + "featuredOperation": { + "title": "Migrating a stateful Nextcloud service", + "context": "The existing standalone deployment contained much more than files: contacts, calendars, tasks, and collaboration data all had to survive a move to Nextcloud All-in-One.", + "actions": [ + "Aligned application versions and updated installed applications before migration.", + "Backed up the source deployment and prepared rollback checkpoints.", + "Converted the existing MySQL database to PostgreSQL before the All-in-One migration.", + "Exported and imported application state, transferred the data directory, corrected ownership and permissions, and preserved required instance configuration.", + "Validated access to the expected files and services before relying on the new deployment." + ], + "outcome": "The migration completed without data loss. The All-in-One deployment has remained Luca's active Nextcloud instance for roughly a year, including storage, synchronization, contacts, calendars, tasks, and Nextcloud Talk." + } }, "projects": { "selected": [ @@ -98,29 +117,34 @@ "title": "Private self-hosted medication tracker", "visibility": "private", "maturity": "active", - "summary": "A private multi-device medication system for schedules, stock awareness, adherence logging, and shared administration.", - "problem": "Medication routines need a dependable way to handle scheduled and as-needed entries across devices.", - "contribution": "Designed and built the private client, server, synchronization, and administrative workflow.", + "summary": "A solo-built, self-hosted medication-tracking system with an offline-first Flutter client, TypeScript API, PostgreSQL database, and Vue administration interface.", + "problem": "A private medication routine needed dependable offline tracking, reminders, history, and synchronization without turning personal data into a public product.", + "contribution": "Independently designed, implemented, deployed, and operated the complete mobile app, API, administration interface, database, synchronization model, and self-hosted environment.", "technicalDecisions": [ - "Supports profiles, scheduled and as-needed medication, stock tracking, notifications, and logged activity.", - "Keeps a server component and administrative interface alongside the multi-device experience." + "Use Flutter, Riverpod, and Drift/SQLite so the mobile workflow remains useful offline and can queue changes before pulling server updates.", + "Use Fastify, TypeScript, Drizzle, and PostgreSQL for the API and synchronized data model, alongside a Vue administration interface.", + "Apply authentication, refresh-token, TOTP, backup-code, biometric-lock, and database access-boundary mechanisms while continuing to harden the system through tests and audit findings.", + "Run the containerized service behind Nginx/TLS and an additional Authelia access layer.", + "Support schedules, as-needed entries, stock tracking, dose history, reminders, background sync, exports, analytics, an Android widget, and English/Romanian localization." ], - "outcome": "The product remains private; this portfolio presents its scope without a public link or media.", + "outcome": "The system has supported Luca's private medication tracking consistently for several months. Real use exposed Android reminder failures; investigation restored dependable delivery, and current work is replacing the remaining recurring behavior with occurrence-specific reconciliation to address duplicate and stale reminders. The project works for its current private use but is not presented as ready for production.", "assetIds": [] }, { "id": "footy-tracker", "title": "Footy Tracker", "visibility": "public", - "maturity": "work-in-progress", - "summary": "A work-in-progress computer-vision pipeline for football footage.", - "problem": "Football footage needs a repeatable way to detect and follow players over time.", - "contribution": "Built the player detection and tracking pipeline and continue to explore jersey recognition.", + "maturity": "active", + "summary": "A two-person university computer-vision project that processed previously unseen football footage and generated an annotated output video.", + "problem": "Build a time-constrained demonstration that could identify football players, referees, and the ball in match footage and explore jersey-number recognition.", + "contribution": "Led the project's data and machine-learning work: dataset acquisition and conversion, YOLO configuration and detector training, jersey-classifier design, experimentation, checkpointing, evaluation, and demo integration. A collaborator created the initial repository structure and video-processing pipeline.", "technicalDecisions": [ - "Separates player detection and tracking from the ongoing jersey-recognition work.", - "Keeps the pipeline focused on footage analysis rather than claiming production readiness." + "Convert SoccerNet tracking data into a reproducible YOLO training layout and keep training behavior configuration-driven.", + "Compare pretrained ResNet, MobileNet, and EfficientNet variants for jersey classification.", + "Add class weighting, checkpoint/resume, focal loss, mixup/cutmix, label smoothing, and early stopping as controlled experiments rather than assuming each technique would improve the model.", + "Prefer the simpler ResNet34 configuration after an eight-hour ablation set showed that aggressive augmentation increased overfitting and reduced validation performance." ], - "outcome": "Player detection and tracking are the current focus; jersey recognition remains ongoing work.", + "outcome": "The submitted demo processed previously unseen, compressed broadcast footage and retained strong player detection. Measured validation results were 0.942 mAP@0.5 for players, 0.726 for referees, and 0.198 for the ball. The best jersey classifier reached approximately 0.778 validation accuracy, while integrated jersey labels remained inconsistent. The result is a working academic demonstration, not a production tracking system.", "url": "https://github.com/LucaPisl/footy-tracker", "assetIds": [] }, @@ -129,14 +153,15 @@ "title": "Opinion Dynamics Simulation", "visibility": "public", "maturity": "experimental", - "summary": "An experimental agent-based simulation for exploring how group opinions change.", - "problem": "Explore how local interactions can produce different group-level opinion patterns.", - "contribution": "Built the experimental model and the result views used to inspect its runs.", + "summary": "An agent-based research experiment for observing how local interactions, network structure, and cascade conditions shape group-level opinion patterns.", + "problem": "Explore how simplified local rules can produce polarization, rapid cascades, and uneven final distributions.", + "contribution": "Built the simulation and analysis views used to inspect modeled runs and compare changes over time.", "technicalDecisions": [ - "Treats the work as an experimental model rather than a predictive product.", - "Uses structured result views to compare changing variables and observed trends." + "Keep the work explicitly experimental rather than presenting it as a predictive model of real people.", + "Expose polarization, cascade thresholds, graph degree, revolt frequency, and final distributions as inspectable outputs.", + "Document performance and network-formation limitations alongside the results." ], - "outcome": "The project is presented as research exploration, with conclusions limited to the modeled scenarios.", + "outcome": "The project produced useful exploratory visualizations and made the model's assumptions and limitations visible; conclusions remain limited to the simulated scenarios.", "url": "https://github.com/LucaPisl/opinion-dynamics-simulation", "assetIds": [] } @@ -147,13 +172,13 @@ "title": "Private game prototype", "visibility": "private", "maturity": "prototype", - "summary": "A private three-week prototype focused on testing a compact gameplay loop.", + "summary": "A private three-week prototype built to test a compact gameplay loop under a deliberately small scope.", "problem": "Test a game concept quickly while keeping the scope deliberately small.", "contribution": "Built and iterated on the prototype during a three-week development window.", "technicalDecisions": [ "Focused the work on a short gameplay loop rather than a full production release." ], - "outcome": "The prototype remains private and is described without a public link or media.", + "outcome": "No real title, link, video, or identifying media.", "assetIds": [] }, { @@ -161,13 +186,13 @@ "title": "Private healthcare platform", "visibility": "private", "maturity": "active", - "summary": "A private web platform presented here only at a high level.", + "summary": "A confidentiality-safe summary of Luca's web-interface contribution.", "problem": "A healthcare workflow needed a focused web-based interface.", - "contribution": "Contributed to the private platform's web experience.", + "contribution": "Contributed to the private platform's web-interface experience.", "technicalDecisions": [ "Keeps product details and any identifying interface data outside the public portfolio." ], - "outcome": "The platform remains private and has no public link or media.", + "outcome": "No product identity, private link, screenshot, or health data.", "assetIds": [] }, { @@ -175,13 +200,11 @@ "title": "Currency Converter", "visibility": "public", "maturity": "learning", - "summary": "A modest .NET and Avalonia learning project.", - "problem": "Practice a focused desktop workflow for currency conversion.", + "summary": "A scoped .NET/Avalonia desktop project integrating external exchange-rate data and practicing typed UI development.", + "problem": "Practice a focused desktop workflow for currency conversion with external exchange-rate data.", "contribution": "Implemented the application as a scoped learning exercise.", - "technicalDecisions": [ - "Uses .NET and Avalonia to explore a typed desktop interface." - ], - "outcome": "A deliberately modest project for developing desktop application fundamentals.", + "technicalDecisions": ["Uses .NET and Avalonia to explore a typed desktop interface."], + "outcome": "A scoped project for developing desktop application fundamentals.", "url": "https://github.com/LucaPisl/CurrencyConverterAvalonia", "assetIds": [] }, @@ -190,13 +213,13 @@ "title": "Linux Modding Guide", "visibility": "public", "maturity": "active", - "summary": "Community documentation that shares Linux troubleshooting and technical writing practice.", + "summary": "Ongoing community documentation focused on practical Linux troubleshooting and reusable technical explanations.", "problem": "Useful Linux troubleshooting knowledge benefits from clear, reusable documentation.", "contribution": "Wrote and maintained community-facing technical guidance.", "technicalDecisions": [ "Favors practical explanations that help readers understand and resolve issues." ], - "outcome": "An ongoing example of technical writing without relying on popularity counts.", + "outcome": "An ongoing example of practical technical writing.", "url": "https://github.com/LucaPisl/LinuxModdingGuide", "assetIds": [] } @@ -204,35 +227,59 @@ }, "toolkit": [ { - "title": "Systems & delivery", + "title": "Systems and delivery", "technologies": [ { "name": "Linux" }, { "name": "Docker" }, { "name": "Nginx" }, - { "name": "Proxmox / KVM" }, + { "name": "cPanel" }, + { "name": "DNS" }, + { "name": "TLS" }, + { "name": "Authelia" }, + { "name": "Fail2Ban" }, + { "name": "Btrfs" }, + { "name": "rsync" }, + { "name": "Nextcloud" }, + { "name": "Proxmox/KVM" }, { "name": "Git" } ] }, { - "title": "Software & data", + "title": "Applications", "technologies": [ { "name": "Python" }, + { "name": "TypeScript/JavaScript" }, { "name": "Vue.js" }, - { "name": "JavaScript" }, - { "name": "C# / .NET" }, - { "name": "Flutter / Dart" }, - { "name": "PostgreSQL / SQL" }, + { "name": "Flutter/Dart" }, + { "name": "Fastify" }, + { "name": "C#/.NET" }, + { "name": "Java" }, { "name": "Headless WordPress" } ] }, { - "title": "Additional experience", + "title": "Data and storage", "technologies": [ - { "name": "Java" } + { "name": "PostgreSQL" }, + { "name": "SQL" }, + { "name": "Drift/SQLite" }, + { "name": "Drizzle ORM" }, + { "name": "SPSS" }, + { "name": "Microsoft Excel" } + ] + }, + { + "title": "Machine learning", + "technologies": [ + { "name": "PyTorch" }, + { "name": "Ultralytics/YOLO" }, + { "name": "OpenCV" }, + { "name": "SoccerNet" } ] } ], "contact": { + "introduction": "I’m open to System Administrator and Full-Stack Developer opportunities in Bucharest, including on-site, hybrid, and remote work, as well as international remote roles.", "email": "luca.pislaru@gmail.com", "social": [ { "service": "GitHub", "url": "https://github.com/LucaPisl" }, @@ -243,7 +290,7 @@ }, "metadata": { "title": "Luca Pîslaru — System Administrator & Full-Stack Developer", - "description": "Portfolio of Luca Pîslaru, a System Administrator & Full-Stack Developer building and operating reliable systems and software." + "description": "Portfolio of Luca Pîslaru, a Bucharest-based System Administrator and Full-Stack Developer working across Linux infrastructure and end-to-end software delivery." }, "soundtrack": { "intro": { diff --git a/docs/content-approval-checklist.md b/docs/content-approval-checklist.md index 12d7933..6442f40 100644 --- a/docs/content-approval-checklist.md +++ b/docs/content-approval-checklist.md @@ -6,20 +6,22 @@ This checklist records the Phase 1 public projection. Every item is either inclu | --- | --- | --- | | Identity | Luca Pîslaru | approved | | Identity | System Administrator & Full-Stack Developer | approved | -| Identity | Building and operating reliable systems and software | approved | +| Identity | Bucharest, Romania | approved | +| Identity | On-site, hybrid, and remote availability | approved | | Profile | Education | approved | | Profile | Languages | approved | -| Profile | PCEP — Certified Entry-Level Python Programmer | approved | -| Experience | Full-Stack Developer & DevOps collaborator at Mihai Razus PFA | approved | +| Profile | Advanced Digital Competencies Certificate and PCEP - Certified Entry-Level Python Programmer | approved | +| Experience | Full-Stack Developer & Hosting Administrator (Collaborator) at Trupa lui Răzuș | approved | | Experience | Green Innovation Intern at Camera Consultanților Fiscali | approved | | Experience | Data Science Intern at iSense Solutions | approved | | Systems operations | Professional systems operations | approved | | Infrastructure | Ingress and TLS | approved | | Infrastructure | Identity and access | approved | | Infrastructure | Service lifecycle | approved | -| Infrastructure | Operations | approved | -| Selected build | Private self-hosted medication tracker | approved | -| Selected build | Footy Tracker | approved | +| Infrastructure | Nextcloud migration completed without data loss and active for roughly a year | approved | +| Infrastructure | Operations and recovery | approved | +| Selected build | Private self-hosted medication tracker - private, active, and not ready for production | approved | +| Selected build | Footy Tracker collaboration and measured detector results | approved | | Selected build | Opinion Dynamics Simulation | approved | | Additional work | Private game prototype | approved | | Additional work | Private healthcare platform | approved | @@ -47,4 +49,5 @@ This checklist records the Phase 1 public projection. Every item is either inclu | Metadata | Page description | approved | | Metadata | Social preview asset | omit | | Assets | Public asset records | omit | -| Assets | Project, toolkit, and metadata asset references | omit | +| Assets | Project and social media | pending approval | +| Assets | Toolkit and metadata asset references | omit | diff --git a/src/content/schemas.ts b/src/content/schemas.ts index 04dff9b..b523c4a 100644 --- a/src/content/schemas.ts +++ b/src/content/schemas.ts @@ -116,6 +116,7 @@ const projectSchema = z.object({ url: httpUrl.optional(), assetIds: z.array(opaqueId), media: z.array(mediaPresentationSchema).optional(), + mediaLayout: z.enum(['single', 'portrait-strip', 'chart-pair']).optional(), }).strict().superRefine((project, context) => { if (project.visibility === 'private' && project.url !== undefined) { context.addIssue({ code: 'custom', message: 'A private project may not include a url.', path: ['url'] }); @@ -138,6 +139,13 @@ const projectSchema = z.object({ }); } } + if (project.mediaLayout !== undefined && (project.media === undefined || project.media.length === 0)) { + context.addIssue({ + code: 'custom', + message: 'Project mediaLayout requires at least one media presentation.', + path: ['mediaLayout'], + }); + } }); const portfolioContentSchema = z.object({ @@ -145,6 +153,12 @@ const portfolioContentSchema = z.object({ name: z.literal('Luca Pîslaru'), role: z.literal('System Administrator & Full-Stack Developer'), valueProposition: visibleString, + location: z.object({ + city: visibleString, + country: visibleString, + countryCode: z.string().regex(/^[A-Z]{2}$/, 'Country code must be a two-letter uppercase code.'), + }).strict(), + availability: visibleString, }).strict(), navigation: z.array(navigationItemSchema).length(8), profile: z.object({ @@ -170,6 +184,12 @@ const portfolioContentSchema = z.object({ title: z.literal('Secure self-hosted platform and lifecycle'), summary: visibleString, bands: z.array(z.object({ id: opaqueId, title: visibleString, summary: visibleString }).strict()).length(4), + featuredOperation: z.object({ + title: visibleString, + context: visibleString, + actions: z.array(visibleString).min(3).max(6), + outcome: visibleString, + }).strict(), }).strict(), projects: z.object({ selected: z.array(projectSchema).length(3), @@ -180,6 +200,7 @@ const portfolioContentSchema = z.object({ technologies: z.array(z.object({ name: visibleString, assetId: opaqueId.optional() }).strict()).min(1), }).strict()).min(1), contact: z.object({ + introduction: visibleString, email: z.email(), social: z.array(z.object({ service: z.enum(['GitHub', 'GitLab', 'LinkedIn']), diff --git a/src/content/types.ts b/src/content/types.ts index 144cc3c..6dc2d94 100644 --- a/src/content/types.ts +++ b/src/content/types.ts @@ -20,6 +20,7 @@ export type SectionLayout = export type ProjectVisibility = 'public' | 'private'; export type ProjectMaturity = 'learning' | 'experimental' | 'prototype' | 'work-in-progress' | 'active'; +export type ProjectMediaLayout = 'single' | 'portrait-strip' | 'chart-pair'; export interface MediaPresentationBase { assetId: string; @@ -84,11 +85,19 @@ export interface InfrastructureEvidenceBand { summary: string; } +export interface FeaturedOperation { + title: string; + context: string; + actions: readonly string[]; + outcome: string; +} + export interface InfrastructureCaseStudy { id: string; title: string; summary: string; bands: readonly InfrastructureEvidenceBand[]; + featuredOperation: FeaturedOperation; } export interface PublicProject { @@ -104,6 +113,7 @@ export interface PublicProject { url?: string; assetIds: readonly string[]; media?: readonly MediaPresentation[]; + mediaLayout?: ProjectMediaLayout; } export interface ToolkitTechnology { @@ -122,6 +132,7 @@ export interface ContactSocialLink { } export interface ContactContent { + introduction: string; email: string; social: readonly ContactSocialLink[]; cvMessage: string; @@ -150,6 +161,8 @@ export interface PortfolioContent { name: 'Luca Pîslaru'; role: 'System Administrator & Full-Stack Developer'; valueProposition: string; + location: { city: string; country: string; countryCode: string }; + availability: string; }; navigation: ReadonlyArray<{ id: SectionId; label: string }>; profile: { introduction: string; screeningFacts: ReadonlyArray<{ label: string; value: string }> }; diff --git a/tests/fixtures/portfolio-content.valid.json b/tests/fixtures/portfolio-content.valid.json index 2131a25..6c152b1 100644 --- a/tests/fixtures/portfolio-content.valid.json +++ b/tests/fixtures/portfolio-content.valid.json @@ -2,7 +2,9 @@ "identity": { "name": "Luca Pîslaru", "role": "System Administrator & Full-Stack Developer", - "valueProposition": "Building and operating reliable systems and software." + "valueProposition": "Building and operating reliable systems and software.", + "location": { "city": "Bucharest", "country": "Romania", "countryCode": "RO" }, + "availability": "Based in Bucharest and open to remote roles." }, "navigation": [ { "id": "start", "label": "Start" }, @@ -41,7 +43,13 @@ { "id": "identity-access", "title": "Identity and access", "summary": "Protected service access." }, { "id": "service-lifecycle", "title": "Service lifecycle", "summary": "Maintained representative services." }, { "id": "operations", "title": "Operations", "summary": "Resolved routine service issues." } - ] + ], + "featuredOperation": { + "title": "Synthetic stateful service migration", + "context": "A representative migration preserved essential application state.", + "actions": ["Prepared versions.", "Created recovery checkpoints.", "Validated the new service."], + "outcome": "The synthetic migration retained expected service data." + } }, "projects": { "selected": [ @@ -55,7 +63,8 @@ "contribution": "Designed and maintained the application.", "technicalDecisions": ["Used a synchronised client and server design."], "outcome": "The work remains private.", - "assetIds": ["selected-build-one"] + "assetIds": ["selected-build-one"], + "media": [{ "kind": "image", "assetId": "selected-build-one", "alt": "Synthetic project evidence." }] }, { "id": "public-build-two", @@ -101,6 +110,7 @@ "technologies": [{ "name": "Linux", "assetId": "toolkit-linux" }] }], "contact": { + "introduction": "Open to System Administrator and Full-Stack Developer opportunities.", "email": "hello@example.test", "social": [{ "service": "GitHub", "url": "https://github.com/example" }], "cvMessage": "For a current CV or role-specific information, contact me directly." diff --git a/tests/integration/public-content.test.ts b/tests/integration/public-content.test.ts index 2bec1a3..fb84965 100644 --- a/tests/integration/public-content.test.ts +++ b/tests/integration/public-content.test.ts @@ -15,10 +15,23 @@ describe('approved public portfolio projection', () => { new URL('file:///prepared-assets/'), ); - expect(content.identity).toEqual(expect.objectContaining({ + expect(content.identity).toMatchObject({ name: 'Luca Pîslaru', role: 'System Administrator & Full-Stack Developer', - })); + location: { city: 'Bucharest', country: 'Romania', countryCode: 'RO' }, + }); + expect(content.experience[0]).toMatchObject({ + organization: 'Trupa lui Răzuș', + role: 'Full-Stack Developer & Hosting Administrator (Collaborator)', + }); + expect(content.infrastructure.featuredOperation.outcome).toMatch(/without data loss.*roughly a year/iu); + expect(content.projects.selected.map(({ id }) => id)).toEqual([ + 'private-medication-tracker', + 'footy-tracker', + 'opinion-dynamics-simulation', + ]); + expect(content.projects.selected[1]?.contribution).toMatch(/collaborator/iu); + expect(content.projects.selected[1]?.outcome).toMatch(/0\.942 mAP@0\.5.*0\.198/iu); expect(content.systemsOperations.title).toBe('Professional systems operations'); expect(content.infrastructure.title).toBe('Secure self-hosted platform and lifecycle'); expect(content.projects.selected).toHaveLength(3); @@ -59,7 +72,9 @@ describe('approved public portfolio projection', () => { canonicalUrl: 'https://portfolio.localhost/', buildId: 'preview-semantic-v1', }); - expect(JSON.stringify(content)).not.toMatch(/Arch Linux|CV download|heatmap|expert|percent/i); + expect(JSON.stringify(content)).not.toMatch( + /Arch Linux|Mihai Razus PFA|DevOps collaborator|CV download|heatmap|fully secure|production-ready|expert|percent/iu, + ); }); it('publishes only the four verified owner repositories and no private project links', async () => { diff --git a/tests/unit/components.test.ts b/tests/unit/components.test.ts index 0c65a82..ea66b9e 100644 --- a/tests/unit/components.test.ts +++ b/tests/unit/components.test.ts @@ -37,6 +37,12 @@ const infrastructure: InfrastructureCaseStudy = { summary: "Routine maintenance and investigation.", }, ], + featuredOperation: { + title: "Synthetic stateful service migration", + context: "A representative migration preserved application state.", + actions: ["Prepared versions.", "Created recovery checkpoints.", "Validated the new service."], + outcome: "The synthetic migration retained expected data.", + }, }; const privateProject: PublicProject = { @@ -159,6 +165,7 @@ describe("semantic section components", () => { it("renders a visible mailto contact and safe verified social links", () => { const contact: ContactContent = { + introduction: "Open to System Administrator and Full-Stack Developer opportunities.", email: "hello@example.test", cvMessage: "For a current CV or role-specific information, contact me directly.", diff --git a/tests/unit/content-schemas.test.ts b/tests/unit/content-schemas.test.ts index 9b6af50..89e4182 100644 --- a/tests/unit/content-schemas.test.ts +++ b/tests/unit/content-schemas.test.ts @@ -15,11 +15,23 @@ const validContent = fixture('portfolio-content.valid.json'); const validManifest = fixture('asset-manifest.valid.json'); const validSite = fixture('site-config.preview.json'); type MutableContentFixture = { + identity: { + location: { city: string; country: string; countryCode: string }; + availability: string; + }; navigation: Array<{ id: string }>; profile: { introduction: string }; - contact: { social: Array<{ service: string; url: string }> }; + contact: { introduction: string; social: Array<{ service: string; url: string }> }; + infrastructure: { featuredOperation: { actions: string[] } }; projects: { - selected: Array<{ visibility: string; url?: string; assetIds: string[]; title: string }>; + selected: Array<{ + visibility: string; + url?: string; + assetIds: string[]; + title: string; + media?: Array<{ assetId: string; alt: string; kind: 'image' }>; + mediaLayout?: 'single' | 'portrait-strip' | 'chart-pair'; + }>; additional: Array<{ assetIds: string[] }>; }; toolkit: Array<{ technologies: Array<{ assetId?: string }> }>; @@ -41,6 +53,25 @@ describe('content schemas', () => { expect(parseSiteConfig(validSite).environment).toBe('preview'); }); + it('requires public availability, city-level location, featured operations evidence, and contact copy', () => { + const content = cloneContent(); + expect(content.identity.location).toEqual({ city: 'Bucharest', country: 'Romania', countryCode: 'RO' }); + expect(content.identity.availability).toMatch(/Bucharest.*remote/iu); + expect(content.infrastructure.featuredOperation.actions.length).toBeGreaterThanOrEqual(3); + expect(content.contact.introduction).toMatch(/System Administrator.*Full-Stack Developer/iu); + + delete (content.identity as Partial).availability; + expect(() => parsePortfolioContent(content)).toThrow(); + }); + + it('accepts only the approved project media layouts', () => { + const content = cloneContent(); + content.projects.selected[0]!.mediaLayout = 'portrait-strip'; + expect(() => parsePortfolioContent(content)).not.toThrow(); + content.projects.selected[0]!.mediaLayout = 'carousel' as never; + expect(() => parsePortfolioContent(content)).toThrow(/mediaLayout|unrecognized/iu); + }); + it('accepts an empty manifest before Phase 3 assets are prepared', () => { expect(parsePublicAssetManifest({ assets: [] })).toEqual({ assets: [] }); }); @@ -117,6 +148,7 @@ describe('content schemas', () => { const content = cloneContent(); content.projects.selected[0]!.title = 'Private self-hosted medication tracker'; for (const project of [...content.projects.selected, ...content.projects.additional]) project.assetIds = []; + delete content.projects.selected[0]!.media; delete content.toolkit[0]!.technologies[0]!.assetId; delete content.metadata.socialPreviewAssetId; expect(() => validateProductionInputs(content, { assets: [] }, validSite, new URL('file:///prepared/'))).not.toThrow(); @@ -126,6 +158,7 @@ describe('content schemas', () => { const content = cloneContent(); content.projects.selected[0]!.title = 'Private self-hosted medication tracker'; for (const project of [...content.projects.selected, ...content.projects.additional]) project.assetIds = []; + delete content.projects.selected[0]!.media; content.toolkit[0]!.technologies[0]!.assetId = 'missing-toolkit-asset'; expect(() => validateProductionInputs(content, { assets: [] }, validSite, new URL('file:///prepared/'))) .toThrow(/unmanifested asset reference: missing-toolkit-asset/i); @@ -205,6 +238,7 @@ describe('content schemas', () => { it('rejects an asset reference absent from the manifest', () => { const content = cloneContent(); content.projects.selected[0]!.assetIds = ['missing-asset']; + content.projects.selected[0]!.media![0]!.assetId = 'missing-asset'; expect(() => validateProductionInputs(content, validManifest, validSite, new URL('file:///prepared/'))) .toThrow(/unmanifested asset/i); }); @@ -217,6 +251,7 @@ describe('content schemas', () => { const unreferencedManifestAsset = cloneContent(); unreferencedManifestAsset.projects.selected[0]!.assetIds = []; + delete unreferencedManifestAsset.projects.selected[0]!.media; expect(() => validateProductionInputs(unreferencedManifestAsset, validManifest, validSite, new URL('file:///prepared/'))) .toThrow(/unreferenced manifest asset/i); });