summaryrefslogtreecommitdiff
path: root/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'src/pages')
-rw-r--r--src/pages/404.astro67
-rw-r--r--src/pages/api/update.ts200
-rw-r--r--src/pages/history/[date].astro13
-rw-r--r--src/pages/index.astro11
-rw-r--r--src/pages/privacy.astro40
5 files changed, 331 insertions, 0 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..0a78f9f
--- /dev/null
+++ b/src/pages/history/[date].astro
@@ -0,0 +1,13 @@
+---
+import Layout from '../../layouts/Layout.astro';
+import { Geshem } from '../../components/Geshem';
+import '../../styles/global.css';
+
+export const prerender = false;
+
+const { date } = Astro.params;
+---
+
+<Layout title="Geshem - Rain Radar History" description="Israel Rain Radar History">
+ <Geshem client:load date={date} />
+</Layout> \ No newline at end of file
diff --git a/src/pages/index.astro b/src/pages/index.astro
new file mode 100644
index 0000000..5b08f18
--- /dev/null
+++ b/src/pages/index.astro
@@ -0,0 +1,11 @@
+---
+import Layout from "../layouts/Layout.astro";
+import { Geshem } from "../components/Geshem";
+import "../styles/global.css";
+
+const { date } = Astro.params;
+---
+
+<Layout>
+ <Geshem client:load date={date} />
+</Layout>
diff --git a/src/pages/privacy.astro b/src/pages/privacy.astro
new file mode 100644
index 0000000..d01a26b
--- /dev/null
+++ b/src/pages/privacy.astro
@@ -0,0 +1,40 @@
+---
+
+---
+
+<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>
+ <script src="https://cdn.usefathom.com/script.js" data-site="MBXPLVRM" defer
+ ></script>
+ <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>