diff options
| author | Yuval Adam <_@yuv.al> | 2025-09-24 09:12:44 +0200 |
|---|---|---|
| committer | Yuval Adam <_@yuv.al> | 2025-09-24 09:12:44 +0200 |
| commit | 4c5be8cf8d942743398581b46ac2c76d0ff273d8 (patch) | |
| tree | b182383a63302f24910c5534953d3624cb24f00d /src/pages | |
| parent | 490214f0a9be03130cf535bc5ae0b8aa3c6c16db (diff) | |
Initial migration
Diffstat (limited to 'src/pages')
| -rw-r--r-- | src/pages/404.astro | 67 | ||||
| -rw-r--r-- | src/pages/api/update.ts | 200 | ||||
| -rw-r--r-- | src/pages/history/[date].astro | 23 | ||||
| -rw-r--r-- | src/pages/index.astro | 11 | ||||
| -rw-r--r-- | src/pages/privacy.astro | 38 |
5 files changed, 336 insertions, 3 deletions
diff --git a/src/pages/404.astro b/src/pages/404.astro new file mode 100644 index 0000000..4db6121 --- /dev/null +++ b/src/pages/404.astro @@ -0,0 +1,67 @@ +--- +--- + +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <link rel="shortcut icon" href="/favicon.ico"> + <title>404 - Page Not Found | geshem.space</title> + <style> + body { + margin: 0; + padding: 0; + background-color: #1a1a1a; + color: white; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + text-align: center; + } + + .container { + max-width: 500px; + } + + h1 { + font-size: 6rem; + margin: 0; + font-weight: bold; + opacity: 0.8; + } + + h2 { + font-size: 1.5rem; + margin: 1rem 0; + font-weight: normal; + } + + p { + margin: 1rem 0; + opacity: 0.7; + line-height: 1.5; + } + + a { + color: #3498db; + text-decoration: none; + font-weight: 500; + } + + a:hover { + text-decoration: underline; + } + </style> + </head> + <body> + <div class="container"> + <h1>404</h1> + <h2>Page Not Found</h2> + <p>The page you're looking for doesn't exist.</p> + <p><a href="/">← Back to Rain Radar</a></p> + </div> + </body> +</html>
\ No newline at end of file diff --git a/src/pages/api/update.ts b/src/pages/api/update.ts new file mode 100644 index 0000000..88dfa3c --- /dev/null +++ b/src/pages/api/update.ts @@ -0,0 +1,200 @@ +import type { APIRoute } from 'astro'; + +export const prerender = false; + +interface ImageData { + [key: string]: string[]; +} + +interface CloudflareEnv { + BASE_URL: string; + AUTH_USER: string; + AUTH_PASS: string; + BUCKET_NAME: string; + SCHEDULED?: boolean; +} + +class GeshemUpdate { + private bucket: R2Bucket; + private env: CloudflareEnv; + + constructor(bucket: R2Bucket, env: CloudflareEnv) { + this.bucket = bucket; + this.env = env; + } + + private async authenticate(request: Request): Promise<Request> { + const headers = new Headers(request.headers); + const credentials = btoa(`${this.env.AUTH_USER}:${this.env.AUTH_PASS}`); + headers.set('Authorization', `Basic ${credentials}`); + + return new Request(request.url, { + method: request.method, + headers, + body: request.body + }); + } + + private async getLatestImages(): Promise<string[]> { + const authenticatedRequest = await this.authenticate(new Request(this.env.BASE_URL)); + const response = await fetch(authenticatedRequest); + const html = await response.text(); + + const regex = /radar280comp_\d+\.png/g; + const matches = html.match(regex) || []; + const uniqueImages = [...new Set(matches)]; + + return uniqueImages.sort().slice(-10); + } + + private async getLatestBucketKeys(): Promise<string[]> { + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + const yesterdayStr = yesterday.toISOString().slice(0, 10).replace(/-/g, ''); + + try { + const objects = await this.bucket.list({ + prefix: 'imgs/', + startAfter: `imgs/${yesterdayStr}` + }); + + return objects.objects.map(obj => obj.key); + } catch (error) { + console.error('Error listing bucket objects:', error); + return []; + } + } + + private async fetchMissingImages(): Promise<boolean> { + const images = await this.getLatestImages(); + const s3Images = await this.getLatestBucketKeys(); + let updated = false; + + for (const img of images) { + const key = this.keyFromFilename(img); + if (!s3Images.includes(key)) { + await this.fetchImage(img, key); + updated = true; + } + } + + return updated; + } + + private async fetchImage(imgName: string, key: string): Promise<void> { + console.log(`Downloading ${imgName} from web server`); + + const imageUrl = `${this.env.BASE_URL}/${imgName}`; + const authenticatedRequest = await this.authenticate(new Request(imageUrl)); + const response = await fetch(authenticatedRequest); + + if (!response.ok) { + throw new Error(`Failed to fetch image: ${response.statusText}`); + } + + const imageData = await response.arrayBuffer(); + + console.log(`Uploading to ${key}`); + await this.bucket.put(key, imageData, { + httpMetadata: { + contentType: 'image/png', + cacheControl: 'public, max-age=31536000' + } + }); + } + + private keyFromFilename(filename: string): string { + const [name, dateStr] = filename.split('.')[0].split('_'); + const res = 280; + + // Parse date: YYYYMMDDHHMM + const year = dateStr.slice(0, 4); + const month = dateStr.slice(4, 6); + const day = dateStr.slice(6, 8); + const hour = dateStr.slice(8, 10); + const minute = dateStr.slice(10, 12); + + const d = `${year}${month}${day}`; + const t = `${hour}${minute}`; + + return `imgs/${d}/${t}/${res}.png`; + } + + private async generateJson(): Promise<void> { + const latestKeys = await this.getLatestBucketKeys(); + const keys = latestKeys + .filter(key => key.endsWith('280.png')) + .sort() + .slice(-10); + + const index: ImageData = { '280': keys }; + + await this.bucket.put('imgs.json', JSON.stringify(index), { + httpMetadata: { + contentType: 'application/json', + cacheControl: 'public, max-age=60' + } + }); + } + + async run(): Promise<string> { + try { + const updated = await this.fetchMissingImages(); + if (updated) { + await this.generateJson(); + } + return `Updated: ${updated}`; + } catch (error) { + console.error('Error in GeshemUpdate.run():', error); + throw error; + } + } +} + +export const GET: APIRoute = async ({ platform, request }) => { + // This endpoint should only be accessible via Cloudflare Cron + const cron = request.headers.get('CF-Cron'); + if (!cron) { + return new Response('Unauthorized', { status: 401 }); + } + + try { + const env = platform?.env as unknown as CloudflareEnv; + + if (!env?.BASE_URL || !env?.AUTH_USER || !env?.AUTH_PASS) { + return new Response('Missing environment variables', { status: 500 }); + } + + // Get R2 bucket (this would be configured in your Cloudflare Workers environment) + const bucket = env.BUCKET_NAME ? platform?.env[env.BUCKET_NAME] as R2Bucket : null; + + if (!bucket) { + return new Response('R2 bucket not configured', { status: 500 }); + } + + const updater = new GeshemUpdate(bucket, env); + const result = await updater.run(); + + return new Response(JSON.stringify({ + message: `SUCCESS: ${result}`, + timestamp: new Date().toISOString() + }), { + status: 200, + headers: { + 'Content-Type': 'application/json' + } + }); + + } catch (error) { + console.error('Error in update handler:', error); + return new Response(JSON.stringify({ + message: 'ERROR', + error: error instanceof Error ? error.message : 'Unknown error' + }), { + status: 500, + headers: { + 'Content-Type': 'application/json' + } + }); + } +};
\ No newline at end of file diff --git a/src/pages/history/[date].astro b/src/pages/history/[date].astro new file mode 100644 index 0000000..0bd1e40 --- /dev/null +++ b/src/pages/history/[date].astro @@ -0,0 +1,23 @@ +--- +import { Geshem } from '../../components/Geshem'; +import '../../styles/global.css'; +import '../../styles/geshem.css'; + +export const prerender = false; + +const { date } = Astro.params; +--- + +<html lang="en"> + <head> + <meta charset="utf-8" /> + <link rel="icon" type="image/png" href="/favicon.png" /> + <meta name="viewport" content="width=device-width" /> + <meta name="generator" content={Astro.generator} /> + <title>Geshem - Rain Radar History</title> + <meta name="description" content="Israel Rain Radar History" /> + </head> + <body> + <Geshem client:load date={date} /> + </body> +</html>
\ No newline at end of file diff --git a/src/pages/index.astro b/src/pages/index.astro index 2d14107..fb90246 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,16 +1,21 @@ --- +import { Geshem } from '../components/Geshem'; +import '../styles/global.css'; +import '../styles/geshem.css'; +const { date } = Astro.params; --- <html lang="en"> <head> <meta charset="utf-8" /> - <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> + <link rel="icon" type="image/png" href="/favicon.png" /> <meta name="viewport" content="width=device-width" /> <meta name="generator" content={Astro.generator} /> - <title>Astro</title> + <title>Geshem - Rain Radar</title> + <meta name="description" content="Israel Rain Radar" /> </head> <body> - <h1>Astro</h1> + <Geshem client:load date={date} /> </body> </html> diff --git a/src/pages/privacy.astro b/src/pages/privacy.astro new file mode 100644 index 0000000..12ac001 --- /dev/null +++ b/src/pages/privacy.astro @@ -0,0 +1,38 @@ +--- + +--- + +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta + name="viewport" + content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" + /> + <meta name="apple-mobile-web-app-title" content="Geshem" /> + <meta name="apple-mobile-web-app-capable" content="yes" /> + <meta name="theme-color" content="#000000" /> + <link rel="shortcut icon" href="/favicon.ico" /> + <link rel="apple-touch-icon" href="/apple-touch-icon.png" /> + <link rel="manifest" href="/manifest.json" /> + <title>Privacy Policy | geshem.space</title> + <style> + body { + overflow: hidden; + position: absolute; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + font-family: sans-serif; + background-color: #1a1a1a; + color: white; + margin: 0; + } + </style> + </head> + <body> + <h1>We do not collect any data.</h1> + </body> +</html> |
