/* Portfolio data and app constants */ /* Portfolio App - Phan Phuoc Thinh */ const PROJECTS = [ { id: 1, title: 'Custom Odoo', category: 'ERP Customization', image: 'assets/project1.png', summary: 'A custom Odoo 19 practice project focused on extending ERP workflows, views, models, and business logic.', description: 'Customized an Odoo 19-based ERP project to practice real module development: extending models, adapting views, wiring business rules, and keeping the code organized as a reusable implementation reference.', features: [ 'Custom Odoo 19 modules and extensions', 'Model inheritance and business field customization', 'XML view inheritance for practical ERP screens', 'PostgreSQL-backed ERP data workflow', 'Reusable GitHub repository for ongoing Odoo practice', ], tech: ['Odoo 19', 'Python', 'XML', 'PostgreSQL', 'JavaScript', 'Git'], role: 'Odoo Customization Developer', duration: 'Personal project', impact: 'Built a focused Odoo customization codebase for ERP practice', repoUrl: 'https://github.com/ppthinh8/odoo-miracle', approach: [ 'Started from Odoo 19 concepts and separated each customization into understandable module-level changes.', 'Practiced model inheritance, XML view inheritance, fields, actions, menus, and security-aware ERP patterns.', 'Kept the repository structured so each change can be reviewed, repeated, and extended in later Odoo phases.', ], outcomes: [ 'Created a public reference repo for Odoo customization practice.', 'Improved confidence with Odoo module anatomy and extension points.', 'Prepared a base for future ERP workflows, reporting, and process-specific custom modules.', ], }, { id: 2, title: 'Project 2 - Data Pipeline Automation', category: 'Data Engineering', image: 'assets/project2.png', summary: 'Automated ETL pipelines reducing manual data processing time by 80% across the organization.', description: 'Designed and implemented a robust data pipeline architecture that automates the extraction, transformation, and loading of business-critical data. Reduced processing time from 8 hours to under 90 minutes while improving data quality and consistency.', features: [ 'Automated ETL workflows with error handling', 'Data quality checks and anomaly detection', 'Scheduling and monitoring dashboard', 'Integration with ERP and CRM systems', 'Incremental load optimization', ], tech: ['Python', 'Apache Airflow', 'PostgreSQL', 'Docker', 'AWS S3'], role: 'Data Engineer', duration: '8 weeks', impact: 'Cut processing time from 8 hours to under 90 minutes', approach: [ 'Separated extraction, validation, transformation, and load steps so each stage could be retried and monitored independently.', 'Added data quality checks for missing keys, duplicate records, and abnormal volume changes before loading downstream tables.', 'Containerized the pipeline runtime to make deployment and rollback more predictable across environments.', ], outcomes: [ 'Reduced repetitive manual processing and made daily data refreshes more reliable.', 'Improved traceability with clearer failure points and operational logs.', 'Prepared the pipeline structure for future ERP and CRM integrations.', ], }, { id: 3, title: 'Project 3 - Predictive Analytics Model', category: 'Machine Learning', image: 'assets/project3.png', summary: 'ML-powered forecasting model improving demand prediction accuracy to 92% for supply chain planning.', description: 'Developed a machine learning solution for demand forecasting that significantly improved inventory planning. The model incorporates seasonal patterns, promotional events, and market trends to generate accurate predictions across 200+ SKUs.', features: [ 'Time-series forecasting with ensemble methods', 'Feature engineering from 30+ business variables', 'A/B testing framework for model validation', 'Automated retraining pipeline', 'Executive reporting with confidence intervals', ], tech: ['Python', 'Scikit-learn', 'XGBoost', 'Pandas', 'Jupyter', 'Tableau'], role: 'Machine Learning Analyst', duration: '10 weeks', impact: 'Reached 92% forecast accuracy in validation', approach: [ 'Explored historical sales, seasonality, promotion windows, and supply constraints to design forecast-ready features.', 'Compared baseline statistical forecasts against tree-based ensemble models before selecting the production candidate.', 'Built reporting views that expose forecast confidence and exception cases instead of only showing point predictions.', ], outcomes: [ 'Improved planning visibility for inventory and replenishment decisions.', 'Made forecast assumptions easier to review with business stakeholders.', 'Created a model workflow that can be retrained as new demand data becomes available.', ], }, ]; const getProjectFromHash = () => { const match = window.location.hash.match(/^#project-(\d+)$/); if (!match) return null; return PROJECTS.find(p => p.id === Number(match[1])) || null; }; const JOURNEY_CODE_STORAGE_KEY = 'ppt-journey-code'; const THEME_STORAGE_KEY = 'ppt-theme-mode'; const PRIORITY_TASK_STORAGE_KEY = 'ppt-priority-tasks-v1'; const DEFAULT_DATABRICKS_JDBC_URL = 'jdbc:databricks://dbc-dcf20014-cc0f.cloud.databricks.com:443/default;transportMode=http;ssl=1;AuthMech=3;httpPath=/sql/1.0/warehouses/691183544b9ff06c;'; const DEFAULT_DATABRICKS_QUERY = [ 'SELECT', ' current_user() AS user,', ' current_catalog() AS catalog,', ' current_schema() AS schema,', ' current_timestamp() AS checked_at', ].join('\n'); const getInitialThemeMode = () => { try { return localStorage.getItem(THEME_STORAGE_KEY) === 'light' ? 'light' : 'dark'; } catch (err) { return 'dark'; } }; const getDefaultJourneyData = () => ({ notes: '', projects: [], progress: [], skills: [], updatedAt: null, }); const createJourneyId = () => `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; const clampProgress = (value) => Math.max(0, Math.min(100, Number(value) || 0)); const normalizeJourneyData = (data = {}) => ({ ...getDefaultJourneyData(), ...data, projects: Array.isArray(data.projects) ? data.projects.map(item => { const safeItem = item || {}; return { id: safeItem.id || createJourneyId(), title: safeItem.title || '', summary: safeItem.summary || '', nextStep: safeItem.nextStep || '', }; }) : [], progress: Array.isArray(data.progress) ? data.progress.map(item => { const safeItem = item || {}; return { id: safeItem.id || createJourneyId(), title: safeItem.title || '', status: safeItem.status || 'Active', progress: clampProgress(safeItem.progress), }; }) : [], skills: Array.isArray(data.skills) ? data.skills.map(item => { const safeItem = item || {}; return { id: safeItem.id || createJourneyId(), name: safeItem.name || '', level: safeItem.level || 'Learning', progress: clampProgress(safeItem.progress), focus: safeItem.focus || '', }; }) : [], }); const JOURNEY_STATUS_OPTIONS = [ { value: 'Active', label: 'Active', color: '#22C55E', bg: 'rgba(34,197,94,0.14)' }, { value: 'Blocked', label: 'Blocked', color: '#FF5252', bg: 'rgba(255,82,82,0.14)' }, { value: 'Done', label: 'Done', color: '#3B82F6', bg: 'rgba(59,130,246,0.14)' }, { value: 'Paused', label: 'Paused', color: '#F59E0B', bg: 'rgba(245,158,11,0.16)' }, ]; const getJourneyStatusOption = (value) => JOURNEY_STATUS_OPTIONS.find(option => option.value === value) || JOURNEY_STATUS_OPTIONS[0]; const getJourneyProgressTone = (value) => { const percent = clampProgress(value); if (percent >= 85) return { color: '#22C55E', bg: 'rgba(34,197,94,0.16)' }; if (percent >= 60) return { color: '#3B82F6', bg: 'rgba(59,130,246,0.15)' }; if (percent >= 30) return { color: '#F5B96B', bg: 'rgba(245,185,107,0.16)' }; return { color: '#FF7A29', bg: 'rgba(255,122,41,0.15)' }; }; const getTodayInputValue = () => { const now = new Date(); const local = new Date(now.getTime() - now.getTimezoneOffset() * 60000); return local.toISOString().slice(0, 10); }; const DE_CONCEPTS = [ { id: 'medallion', title: 'Medallion Architecture', eyebrow: 'Lakehouse layers', summary: 'Follow raw events as they move from landing zones into trusted analytical products.', question: 'How does raw data become business-ready data?', stages: [ { id: 'sources', label: 'Sources', tone: '#94A3B8', subtitle: 'ERP, CRM, logs, files', detail: 'Systems emit data in their own shape. The key job here is capture without over-transforming.', records: ['orders.csv', 'crm_contacts', 'web_events', 'inventory_api'], checks: ['Connectivity', 'Schema arrival', 'Volume baseline'], }, { id: 'bronze', label: 'Bronze', tone: '#CD7F32', subtitle: 'Raw, append-only', detail: 'Bronze preserves source fidelity. It is replayable, auditable, and intentionally close to the original payload.', records: ['raw_order_json', 'ingested_at', 'source_file', 'load_batch_id'], checks: ['No destructive edits', 'Lineage fields', 'Partition by arrival'], }, { id: 'silver', label: 'Silver', tone: '#C0C7D1', subtitle: 'Cleaned and conformed', detail: 'Silver standardizes types, removes duplicates, applies quality rules, and joins reference data.', records: ['order_id', 'customer_id', 'order_date', 'net_amount'], checks: ['Primary keys', 'Deduplication', 'Valid dates', 'Referential checks'], }, { id: 'gold', label: 'Gold', tone: '#F5B96B', subtitle: 'Business-ready marts', detail: 'Gold tables answer business questions. They are modeled for KPIs, dashboards, and downstream analytics.', records: ['daily_revenue', 'customer_ltv', 'sku_margin', 'region_scorecard'], checks: ['Metric definitions', 'Aggregations', 'Access control'], }, { id: 'serving', label: 'Serving', tone: '#22C55E', subtitle: 'BI, ML, reverse ETL', detail: 'Serving systems consume curated data through dashboards, APIs, feature stores, and operational syncs.', records: ['Power BI', 'ML features', 'alerts', 'sync to CRM'], checks: ['SLA monitoring', 'Freshness', 'User adoption'], }, ], metrics: [ ['Freshness', '15 min'], ['Quality gates', '12'], ['Replay window', '30 days'], ['Consumers', 'BI + ML'], ], }, { id: 'databricks_platform', title: 'Databricks Data + AI Platform', eyebrow: 'Unified workspace', summary: 'Map the full Databricks whiteboard: builders work on SQL, AI/BI, Mosaic AI, governed by Unity Catalog, backed by a Delta Lakehouse and managed Spark compute.', question: 'What does Databricks unify for every data role?', stages: [ { id: 'dbx_workspace', label: 'Workspace', tone: '#EF4444', subtitle: 'Shared place to build', detail: 'The workspace is where data scientists, analysts, engineers, and AI engineers open notebooks, dashboards, SQL editors, workflows, and model tools without managing separate platforms.', records: ['Notebooks', 'Dashboards', 'Repos', 'Workflows'], checks: ['Shared context', 'Role access', 'Project isolation'], }, { id: 'dbx_workloads', label: 'Workloads', tone: '#F97316', subtitle: 'SQL, AI/BI, Mosaic AI', detail: 'Databricks groups the main work into workload surfaces: SQL warehouses for analytics, AI/BI for guided exploration, and Mosaic AI for model and agent development.', records: ['SQL Warehouse', 'AI/BI', 'Mosaic AI', 'Model Serving'], checks: ['Right tool for job', 'Cost controls', 'Serving path'], }, { id: 'unity_catalog', label: 'Unity Catalog', tone: '#A855F7', subtitle: 'Governance layer', detail: 'Unity Catalog is the control plane for catalogs, schemas, permissions, lineage, and audits. It lets teams build quickly while security and auditors can still review access.', records: ['Catalogs', 'Schemas', 'Lineage', 'Permissions'], checks: ['Least privilege', 'Ownership', 'Audit trail'], }, { id: 'delta_lakehouse', label: 'Delta Lakehouse', tone: '#38BDF8', subtitle: 'Reliable data tables', detail: 'The lakehouse stores open Delta tables that support batch, streaming, BI, ML, and data sharing. It is the common data foundation under every workload.', records: ['Bronze tables', 'Silver tables', 'Gold marts', 'Feature tables'], checks: ['ACID tables', 'Schema evolution', 'Freshness'], }, { id: 'managed_compute', label: 'Managed Compute', tone: '#22C55E', subtitle: 'Spark without ops burden', detail: 'Databricks manages the runtime, Spark clusters, autoscaling, scheduling, security integration, and cloud execution so builders can focus on project logic.', records: ['Serverless SQL', 'Job clusters', 'All-purpose clusters', 'Apache Spark'], checks: ['Autoscaling', 'Runtime version', 'Cluster policies'], }, ], metrics: [ ['Primary roles', 'DS + DE + BI + AI'], ['Governance', 'Unity Catalog'], ['Storage layer', 'Delta Lake'], ['Compute', 'Managed Spark'], ], }, { id: 'managed_spark', title: 'Managed Spark Projects', eyebrow: 'Build vs manage', summary: 'Show the split from the sketch: data teams build PySpark projects in the workspace while Databricks manages Spark infrastructure, scheduling, security, and scaling.', question: 'Which work belongs to builders, and which work is managed by Databricks?', stages: [ { id: 'builders', label: 'Builders', tone: '#F59E0B', subtitle: 'Data scientists and engineers', detail: 'Builders write notebooks, pipelines, tests, SQL, and project logic. They use PySpark and SQL, but they should not spend their day wiring low-level cluster operations.', records: ['PySpark code', 'SQL models', 'Experiments', 'Pipeline logic'], checks: ['Clear owner', 'Review path', 'Reusable modules'], }, { id: 'dbx_projects', label: 'Projects', tone: '#EF4444', subtitle: 'Workspace artifacts', detail: 'A project bundles notebooks, workflow jobs, source-controlled code, parameters, libraries, and data contracts so work can move from exploration into repeatable production.', records: ['Notebooks', 'Repos', 'Jobs', 'Libraries'], checks: ['Source control', 'Parameterization', 'Promotion path'], }, { id: 'spark_runtime', label: 'Spark Runtime', tone: '#F97316', subtitle: 'Distributed execution', detail: 'Spark distributes transformations across workers, handles shuffles and caching, and executes batch or streaming workloads through the Databricks runtime.', records: ['DataFrames', 'Tasks', 'Shuffles', 'Caching'], checks: ['Partition sizing', 'Shuffle cost', 'Runtime selection'], }, { id: 'managed_cloud', label: 'Managed Cloud', tone: '#38BDF8', subtitle: 'Security, clusters, scaling', detail: 'Databricks handles the managed side: cluster lifecycle, autoscaling, credentials, cloud networking, runtime patches, serverless capacity, and guardrails.', records: ['Autoscaling', 'Policies', 'Credentials', 'Serverless capacity'], checks: ['Policy fit', 'Secret scope', 'Cost alerts'], }, { id: 'operations', label: 'Operations', tone: '#22C55E', subtitle: 'Schedules and monitoring', detail: 'Operations makes projects dependable: schedules, retries, alerts, logs, run history, and production ownership keep pipelines healthy after launch.', records: ['Schedules', 'Retries', 'Alerts', 'Run history'], checks: ['SLA owner', 'Failure alerts', 'Backfill plan'], }, ], metrics: [ ['Builders own', 'Code + logic'], ['Platform owns', 'Runtime + scale'], ['Core API', 'PySpark'], ['Output', 'Projects'], ], }, { id: 'sql_lakehouse', title: 'SQL Warehouse + Lakehouse', eyebrow: 'Analyst workflow', summary: 'Connect the third sketch: analysts query governed lakehouse tables through SQL warehouses while engineers keep Delta Lake and Spark pipelines reliable.', question: 'How do analysts, engineers, and scientists meet on the same lakehouse data?', stages: [ { id: 'roles_to_tools', label: 'Roles to Tools', tone: '#F59E0B', subtitle: 'DS, BI developer, analyst, DE', detail: 'Different roles enter through different tools: data scientists use notebooks, analysts and BI developers use SQL, and engineers keep pipelines moving.', records: ['Data Scientist', 'BI Developer', 'Data Analyst', 'Data Engineer'], checks: ['Tool fit', 'Shared definitions', 'Access scope'], }, { id: 'sql_warehouse', label: 'SQL Warehouse', tone: '#EF4444', subtitle: 'Fast BI serving', detail: 'SQL warehouses provide scalable SQL compute for dashboards, ad hoc analysis, and BI tools without forcing analysts to manage Spark clusters directly.', records: ['SQL editor', 'Dashboards', 'BI connectors', 'Query history'], checks: ['Warehouse size', 'Query cost', 'Concurrency'], }, { id: 'delta_lake', label: 'Delta Lake', tone: '#38BDF8', subtitle: 'Lakehouse tables', detail: 'Delta Lake brings reliable tables to object storage: transactions, schema enforcement, time travel, and streaming/batch access on the same data.', records: ['Delta tables', 'ACID log', 'Time travel', 'Streaming reads'], checks: ['Schema rules', 'Optimize cadence', 'Retention policy'], }, { id: 'pipeline_build', label: 'Pipeline Build', tone: '#F97316', subtitle: 'PySpark and jobs', detail: 'Data engineers build ingestion and transformation pipelines with PySpark, jobs, and workflows so the SQL warehouse always points at fresh trusted tables.', records: ['Ingest jobs', 'Transform jobs', 'DLT pipelines', 'Quality checks'], checks: ['Idempotency', 'Freshness', 'Backfill safety'], }, { id: 'managed_spark_ops', label: 'Managed Spark', tone: '#22C55E', subtitle: 'Databricks manages it', detail: 'The platform manages Spark runtime, cloud infrastructure, cluster lifecycle, security integration, and scaling under both engineering and SQL workloads.', records: ['Spark runtime', 'Job clusters', 'Serverless', 'Cloud storage'], checks: ['Cluster policy', 'Runtime patching', 'Cost guardrail'], }, ], metrics: [ ['Query surface', 'SQL Warehouse'], ['Table format', 'Delta Lake'], ['Pipeline API', 'PySpark'], ['Managed by', 'Databricks'], ], }, { id: 'powerbi', title: 'Power BI Concepts', eyebrow: 'BI workflow', summary: 'Learn how Power BI turns sources into governed semantic models, DAX measures, reports, and shared apps.', question: 'How does data become a trusted Power BI report?', stages: [ { id: 'pbi_sources', label: 'Sources', tone: '#38BDF8', subtitle: 'SQL, Excel, APIs, lakehouse', detail: 'Power BI can connect to many source systems. The first concept is understanding where the data lives, how often it changes, and whether it should be imported or queried directly.', records: ['SQL tables', 'Excel files', 'REST APIs', 'Lakehouse tables'], checks: ['Connector choice', 'Credential scope', 'Refresh path'], }, { id: 'power_query', label: 'Power Query', tone: '#22C55E', subtitle: 'Clean and shape data', detail: 'Power Query is the transformation layer. Learners should focus on applied steps, data types, column cleanup, merge/append logic, and keeping transformations readable.', records: ['Applied steps', 'Changed Type', 'Merge Queries', 'Append Queries'], checks: ['Correct data types', 'Reusable steps', 'No hidden row loss'], }, { id: 'semantic_model', label: 'Semantic Model', tone: '#F5B96B', subtitle: 'Tables and relationships', detail: 'The semantic model defines how users reason about the data. A clean star schema, correct relationships, and clear table names make reports much easier to build.', records: ['FactSales', 'DimDate', 'DimProduct', 'Relationships'], checks: ['Star schema', 'Filter direction', 'Grain clarity'], }, { id: 'dax_measures', label: 'DAX Measures', tone: '#A78BFA', subtitle: 'Business calculations', detail: 'DAX measures encode business logic. The important concepts are filter context, row context, time intelligence, and separating measures from raw columns.', records: ['Total Sales', 'Sales YTD', 'Margin %', 'Customer Count'], checks: ['Filter context', 'Measure table', 'Format strings'], }, { id: 'report_canvas', label: 'Report Canvas', tone: '#EC4899', subtitle: 'Visuals and interactions', detail: 'The report canvas is where the model becomes a user experience. Good reports guide attention with layout, slicers, drill-through, bookmarks, and consistent visual choices.', records: ['Cards', 'Charts', 'Slicers', 'Drill-through pages'], checks: ['Clear KPI hierarchy', 'Accessible colors', 'Useful interactions'], }, { id: 'powerbi_service', label: 'Service', tone: '#F97316', subtitle: 'Publish, refresh, share', detail: 'Power BI Service handles collaboration and operations: workspaces, scheduled refresh, gateways, row-level security, apps, and usage monitoring.', records: ['Workspace', 'Dataset refresh', 'Gateway', 'App audience'], checks: ['Refresh status', 'RLS tested', 'Permission model'], }, ], metrics: [ ['Core language', 'DAX'], ['Model style', 'Star schema'], ['Transform layer', 'Power Query'], ['Delivery', 'Reports + Apps'], ], }, { id: 'orchestration', title: 'Pipeline Orchestration', eyebrow: 'DAG thinking', summary: 'See how dependencies, retries, and observability keep data jobs predictable.', question: 'What should run first, and what happens when it fails?', stages: [ { id: 'extract', label: 'Extract', tone: '#38BDF8', subtitle: 'Pull source changes', detail: 'Extraction tasks collect deltas from APIs, databases, and files with source-specific checkpoints.', records: ['api_cursor', 'watermark', 'source_snapshot'], checks: ['Credential health', 'Rate limits', 'Checkpoint saved'], }, { id: 'validate', label: 'Validate', tone: '#F97316', subtitle: 'Guard the run', detail: 'Validation catches empty loads, schema drift, and abnormal row counts before downstream tables are touched.', records: ['row_count', 'schema_hash', 'null_profile'], checks: ['Schema drift', 'Volume anomaly', 'Required fields'], }, { id: 'transform', label: 'Transform', tone: '#A78BFA', subtitle: 'Build models', detail: 'Transform jobs produce standardized tables, dimensional models, and incremental aggregations.', records: ['stg_orders', 'dim_customer', 'fact_sales'], checks: ['Idempotent logic', 'Incremental keys', 'Unit tests'], }, { id: 'publish', label: 'Publish', tone: '#22C55E', subtitle: 'Release outputs', detail: 'Published datasets are promoted only after dependencies succeed and freshness expectations are met.', records: ['semantic_model', 'dashboard_cache', 'feature_table'], checks: ['SLA met', 'Downstream notify', 'Run metadata'], }, ], metrics: [ ['Retry policy', '3 attempts'], ['Critical path', '42 min'], ['Failed fast checks', '4'], ['Owners', 'Data platform'], ], }, { id: 'quality', title: 'Data Quality Gates', eyebrow: 'Trust controls', summary: 'Watch bad records get quarantined before they corrupt downstream analytics.', question: 'Where should the pipeline stop bad data?', stages: [ { id: 'profile', label: 'Profile', tone: '#38BDF8', subtitle: 'Understand shape', detail: 'Profiling establishes expected distributions, null rates, and uniqueness patterns.', records: ['min/max', 'null_rate', 'unique_count'], checks: ['Baseline stats', 'Field coverage', 'Pattern scan'], }, { id: 'assert', label: 'Assert', tone: '#F5B96B', subtitle: 'Apply rules', detail: 'Assertions define what must be true before data can move forward.', records: ['not_null(order_id)', 'amount >= 0', 'valid_status'], checks: ['Completeness', 'Validity', 'Uniqueness'], }, { id: 'quarantine', label: 'Quarantine', tone: '#FF5252', subtitle: 'Isolate defects', detail: 'Rejected records are stored with failure reasons, not silently dropped.', records: ['bad_record_id', 'rule_failed', 'raw_payload'], checks: ['Error reason', 'Reprocess path', 'Alert owner'], }, { id: 'certify', label: 'Certify', tone: '#22C55E', subtitle: 'Mark trusted', detail: 'Certified datasets carry quality scores, lineage, and freshness metadata for consumers.', records: ['quality_score', 'lineage_id', 'freshness_ts'], checks: ['Score threshold', 'Lineage complete', 'Consumer SLA'], }, ], metrics: [ ['Rules', '18'], ['Quarantine rate', '1.7%'], ['Quality score', '98.3'], ['Alert channel', 'Ops'], ], }, ]; const verifyJourneyCode = async (candidateCode) => { const trimmedCode = candidateCode.trim(); const response = await fetch('/api/journey/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code: trimmedCode }), }); const payload = await response.json().catch(() => ({})); if (!response.ok || payload.ok === false) { throw new Error(payload.error || 'Journey API error'); } return { code: trimmedCode, payload }; };