11import { chromium } from "playwright"
22import path from "node:path"
3+ import fs from "node:fs"
34import {
45 CITY_KYIV , STREET_KYIV , HOUSE_KYIV ,
56 CITY_ODESA , STREET_ODESA , HOUSE_ODESA ,
@@ -488,10 +489,92 @@ function transformYasnoFormat(yasnoRaw) {
488489 return { schedule : scheduleMap , emergency : isEmergency } ;
489490}
490491
492+ // --- ЕКСПОРТ ---
493+ function transformToExportFormat ( schedule , regionId ) {
494+ const exportData = {
495+ regionId : regionId ,
496+ lastUpdated : new Date ( ) . toISOString ( ) ,
497+ fact : {
498+ data : { }
499+ }
500+ } ;
501+
502+ const dates = new Set ( ) ;
503+
504+ for ( const [ groupId , dateMap ] of Object . entries ( schedule ) ) {
505+ for ( const [ dateStr , dailyMap ] of Object . entries ( dateMap ) ) {
506+ // Parse date as Kyiv Midnight
507+ // dateStr is YYYY-MM-DD
508+ // Winter Time (Jan) is UTC+2.
509+ // Simple workaround: Create UTC date and substract 2 hours (7200s) if we assume strict winter time,
510+ // OR better: use date string constructs that Date() accepts.
511+ // "2026-01-30T00:00:00+02:00"
512+ const ts = Math . floor ( new Date ( `${ dateStr } T00:00:00+02:00` ) . getTime ( ) / 1000 ) ;
513+ const timestampKey = ts . toString ( ) ;
514+
515+ if ( ! exportData . fact . data [ timestampKey ] ) {
516+ exportData . fact . data [ timestampKey ] = { } ;
517+ }
518+
519+ const gpvKey = `GPV${ groupId } ` ;
520+ const hoursData = { } ;
521+
522+ for ( let h = 1 ; h <= 24 ; h ++ ) {
523+ const hh = String ( h - 1 ) . padStart ( 2 , '0' ) ;
524+ const val00 = dailyMap [ `${ hh } :00` ] ;
525+ const val30 = dailyMap [ `${ hh } :30` ] ;
526+
527+ let status = 'yes' ; // Default ON
528+ if ( val00 === 1 && val30 === 1 ) status = 'yes' ;
529+ else if ( val00 === 2 && val30 === 2 ) status = 'no' ;
530+ else if ( val00 === 2 && val30 === 1 ) status = 'first' ; // OFF first half = first half is 'shutdown' -> wait.
531+ // User mappings:
532+ // "first": 00-30 OFF (2), 30-00 ON (1) -> "first" status usually means "first half off" or "first half something".
533+ // Let's check user example:
534+ // "GPV2.1" "1": "first" => Val00=?, Val30=?
535+ // In example GPV2.1 hour 1 is 'first'.
536+ // Let's see transformToSvitloFormat logic (reverse):
537+ // case "first": val00 = 2; val30 = 1; (OFF, ON)
538+ // So 'first' maps to [2, 1]
539+ // case "second": val00 = 1; val30 = 2; (ON, OFF)
540+
541+ if ( val00 === 2 && val30 === 1 ) status = 'first' ;
542+ else if ( val00 === 1 && val30 === 2 ) status = 'second' ;
543+
544+ hoursData [ h . toString ( ) ] = status ;
545+ }
546+
547+ exportData . fact . data [ timestampKey ] [ gpvKey ] = hoursData ;
548+ }
549+ }
550+
551+ // Fill summary
552+ const keys = Object . keys ( exportData . fact . data ) . sort ( ) ;
553+ if ( keys . length > 0 ) {
554+ exportData . fact . today = parseInt ( keys [ 0 ] ) ;
555+
556+ const now = new Date ( ) ;
557+ now . setHours ( now . getHours ( ) + 2 ) ; // Quick hack for Kyiv approx if env is UTC, or just use local
558+ // Actually user wanted "30.01.2026 00:02" format.
559+ // Let's just formatting current time.
560+ const d = new Date ( ) ;
561+ const pad = ( n ) => String ( n ) . padStart ( 2 , '0' ) ;
562+ exportData . fact . update = `${ pad ( d . getDate ( ) ) } .${ pad ( d . getMonth ( ) + 1 ) } .${ d . getFullYear ( ) } ${ pad ( d . getHours ( ) ) } :${ pad ( d . getMinutes ( ) ) } ` ;
563+ }
564+
565+ return exportData ;
566+ }
567+
491568// 4. ГОЛОВНИЙ ЗАПУСК
492569async function run ( ) {
493570 console . log ( "🚀 Starting Multi-Region Scraper (Robust Mode with Odesa Fix)..." ) ;
494571
572+ // Ensure artifacts directory exists
573+ const artifactsDir = path . resolve ( "artifacts" ) ;
574+ if ( ! fs . existsSync ( artifactsDir ) ) {
575+ fs . mkdirSync ( artifactsDir ) ;
576+ }
577+
495578 const browser = await chromium . launch ( { headless : true } ) ;
496579 const processedRegions = [ ] ;
497580 const globalDates = { today : null , tomorrow : null } ;
@@ -715,6 +798,15 @@ async function run() {
715798 schedule : chernivtsiSchedule ,
716799 emergency : false
717800 } ) ;
801+
802+ // Save to JSON for repo
803+ try {
804+ const exportData = transformToExportFormat ( chernivtsiSchedule , "Chernivtsi" ) ;
805+ fs . writeFileSync ( path . join ( artifactsDir , "chernivtsi.json" ) , JSON . stringify ( exportData , null , 2 ) ) ;
806+ console . log ( "💾 Saved artifacts/chernivtsi.json" ) ;
807+ } catch ( e ) {
808+ console . error ( "❌ Failed to save artifacts/chernivtsi.json:" , e ) ;
809+ }
718810 }
719811
720812 await browser . close ( ) ;
@@ -737,6 +829,14 @@ async function run() {
737829 timestamp : Date . now ( )
738830 } ;
739831
832+ // Save last-message.json for repo
833+ try {
834+ fs . writeFileSync ( path . join ( artifactsDir , "last-message.json" ) , JSON . stringify ( JSON . parse ( finalOutput . body ) , null , 2 ) ) ;
835+ console . log ( "💾 Saved artifacts/last-message.json" ) ;
836+ } catch ( e ) {
837+ console . error ( "❌ Failed to save artifacts/last-message.json:" , e ) ;
838+ }
839+
740840 if ( ! CF_WORKER_URL || ! CF_WORKER_TOKEN ) {
741841 console . error ( "❌ Missing Cloudflare secrets!" ) ;
742842 process . exit ( 1 ) ;
0 commit comments