# Woodson ISD Navigation Pages (NAVs)

This directory contains dynamic navigation pages that pull content from Google Drive and display it through ParentSquare CMS via iframes.

## 📁 Directory Structure

```
/var/www/woodson/navs/
├── cache/                      # Cached Drive listings, PDFs, and JSON data
│   ├── teacher/               # Teacher resolution cache (syllabi)
│   └── *.json                 # Drive API response caches
├── pdfjs/                     # PDF.js viewer library (local)
├── logs/                      # Application logs
├── *.php                      # Dynamic PHP pages (see below)
├── *.html                     # Static navigation menus
├── preload_syllabi.php        # Background cache warmer
└── purge_cache.php            # Cache cleanup utility

```

---

## 📄 Page Types

### **Type 1: PDF Viewers (Auto-pick Latest by Year)**
These pages automatically select and display the most recent PDF from a Google Drive folder.

| File | Purpose | Search Pattern | Folder ID |
|------|---------|----------------|-----------|
| `family-engagement.php` | Family Engagement Plan | `family[\s_]*(engagement\|involvement)` | `1d7wQCQJtPxHupgFhihT9PKhb0evr_w1g` |
| `student-parent-grievances.php` | Grievance Policy | `(student\|parent).*grievance` | `1d7wQCQJtPxHupgFhihT9PKhb0evr_w1g` |
| `coi.php` | Conflict of Interest | `conflict.*interest` | `1itqVXVP6Mq-OuOqCjV059T3BwoQR41f1` |
| `wellness.php` | Wellness Policy | `wellness.*plan` | (various) |
| `dit.php` | Digital Instruction | `digital.*instruction` | (various) |

**Features:**
- Extracts year from filename (2025, 2025-26, etc.)
- Picks highest year, then latest modified date
- Uses PDF.js for client-side rendering
- PostMessage height to parent iframe
- Cache: 10 min (Drive list), 24 hours (PDF)

**Embed Parameters:**
- `?embed=1` - No header, postMessage height
- `?nocache=1` - Force cache refresh
- `?debug=1` - Show raw Drive JSON

---

### **Type 2: Document Tables (Date-sorted Lists)**
These display tables of files from Google Drive folders, organized by date.

| File | Purpose | Features | Folder ID |
|------|---------|----------|-----------|
| `board-agendas.php` | Board Meeting Agendas | Auto-picks latest year folder, sorts newest first | `1EGwXZe5wND731GbIjeUHB3wECLUM_hlT` |
| `board-minutes.php` | Board Meeting Minutes | Same as agendas | (same parent) |
| `shac-agendas.php` | SHAC Committee Agendas | Parses meeting dates from filenames | `1G3HAKTWn5QmcAXXTYeCSzAM5O8XfR-LD` |
| `shac-minutes.php` | SHAC Committee Minutes | Same as SHAC agendas | (same parent) |

**Features:**
- Automatically finds current school year folder
- Extracts dates from filenames (various formats)
- Sorts by date descending (newest first)
- Responsive table design
- Cache: 10 minutes

**Embed Parameters:**
- `?embed=1` - Minimal styling for iframe
- `?compact=1` - Condensed layout
- `?nocache=1` - Force refresh

---

### **Type 3: Google Sheets Tables**
Pull live data from Google Sheets and render as styled HTML tables.

| File | Purpose | Sheet ID | Features |
|------|---------|----------|----------|
| `meals.php` | Cafeteria Meals & Prices | `1l4ZZCCpNDYMUtnd4-4M_EM1M3vmKin9VUQYX6bY-HYc` | Theme colors, responsive |
| `board-terms.php` | Board Member Terms | (via Sheets API) | Gold theme styling |
| `budgets.php` | District Budgets | (via Sheets API) | Financial data display |

**Features:**
- Google Sheets Visualization API (gviz)
- Auto-detect header rows
- Themed styling (gold/green)
- Cache: 10 minutes

**Embed Parameters:**
- `?embed=1` - Iframe mode
- `?compact=1` - Condensed spacing
- `?theme=green` - Alternative color scheme
- `?sheet=SheetName` or `?gid=123` - Specific tab
- `?noheader=1` - Hide page title

---

### **Type 4: Complex Navigation Systems**
Advanced pages with filtering, search, and multi-level organization.

| File | Purpose | Key Features |
|------|---------|--------------|
| `syllabi.php` | Course Syllabi Browser | Teacher search, course code lookup, PDF preview, auto-year detection |
| `student-handbook.php` | Student Handbook Viewer | Multi-grade levels, year folders |
| `staff-handbook.php` | Staff Handbook Viewer | Role-based sections |
| `calendar.php` | School Calendar | Google Calendar integration |

**Syllabi Features:**
- Parses filenames: `[Course Code] - [Teacher Name].pdf`
- Course code CSV lookup from Google Sheets
- Teacher name resolution (from PDF metadata or content)
- Cached search index (teacher cache)
- PDF.js inline viewer with navigation
- Auto-picks current school year folder

---

### **Type 5: Static Navigation Menus (HTML)**
Pure HTML/CSS/JS accordion menus that link to other pages.

| File | Purpose | Links To |
|------|---------|----------|
| `academic-accountability.html` | Academic Accountability Menu | TAPR reports, Texas dashboards, accountability ratings |
| `finance.html` | Finance Menu | Budget pages, financial reports, transparency links |
| `health.html` | Health Services Menu | Immunization, medication policies, health forms |
| `special-education.html` | Special Education Menu | IDEA, 504, dyslexia, evaluation procedures |

**Features:**
- Collapsible accordion sections
- Mobile-responsive design
- No backend dependencies
- Links to external resources and internal pages

---

## 🔧 Configuration

### Google Drive API Key
All dynamic pages use a **restricted Google API key**:
```php
$GOOGLE_KEY = 'AIzaSyA_5qdRP3WdFJoTBINlUsF8GytbPgeqOro';
```

**Key Restrictions:**
- HTTP referrers: `navs.woodsonisd.net/*`
- API restrictions: Google Drive API v3

### Cache Settings
Default cache lifetimes:
```php
$LIST_TTL = 600;      // 10 minutes - Drive folder listings
$PDF_TTL = 86400;     // 24 hours - Downloaded/converted PDFs
$CODES_TTL = 21600;   // 6 hours - Course code CSV
```

### Cache Directory
```bash
/var/www/woodson/navs/cache/
```
Permissions: `775` (writable by `www-data`)

---

## 🚀 Creating a New Page

### **Template 1: Single PDF Auto-Picker**

Use this template for pages that display ONE PDF that changes yearly.

```php
<?php
/* -----------------------------------------------------------------------------
   Woodson ISD – [PAGE NAME] (Auto-pick latest year)
----------------------------------------------------------------------------- */

/* ====== CONFIG ====== */
$FOLDER_ID  = 'YOUR_GOOGLE_DRIVE_FOLDER_ID_HERE';
$GOOGLE_KEY = 'AIzaSyA_5qdRP3WdFJoTBINlUsF8GytbPgeqOro';
$CACHE_DIR  = __DIR__ . '/cache';
$LIST_TTL   = 600;      // Drive list cache (sec)
$PDF_TTL    = 86400;    // Cached PDF lifetime (sec)

$EMBED = isset($_GET['embed']) && $_GET['embed'] === '1';

/* ====== BOOT ====== */
if (!is_dir($CACHE_DIR)) @mkdir($CACHE_DIR, 0775, true);

/* --- Helpers --- */
function gget(string $url): array {
    $ctx = stream_context_create([
        'http' => ['method'=>'GET','timeout'=>20,'header'=>"Accept: application/json\r\n"]
    ]);
    $raw = @file_get_contents($url, false, $ctx);
    if ($raw === false) return [];
    $data = json_decode($raw, true);
    return is_array($data) ? $data : [];
}

function list_drive_children(string $folderId, string $apiKey, int $pageSize = 200): array {
    $base   = 'https://www.googleapis.com/drive/v3/files';
    $q      = sprintf("'%s' in parents and trashed=false", $folderId);
    $fields = 'nextPageToken,files(id,name,mimeType,modifiedTime,webViewLink,webContentLink,exportLinks)';
    $params = ['q'=>$q,'pageSize'=>$pageSize,'fields'=>$fields,'key'=>$apiKey,'orderBy'=>'modifiedTime desc'];

    $files = [];
    $pageToken = null;
    do {
        $url  = $base.'?'.http_build_query(array_filter($params + ['pageToken'=>$pageToken]));
        $resp = gget($url);
        if (!empty($resp['files'])) $files = array_merge($files, $resp['files']);
        $pageToken = $resp['nextPageToken'] ?? null;
    } while ($pageToken);

    return $files;
}

function cached_list(string $folderId, string $apiKey, string $cacheDir, int $ttl): array {
    $cacheFile = $cacheDir . '/YOUR_PAGE_NAME_list.json';
    if (is_file($cacheFile) && (time() - filemtime($cacheFile) < $ttl)) {
        $data = json_decode(@file_get_contents($cacheFile), true);
        if (is_array($data)) return $data;
    }
    $data = list_drive_children($folderId, $apiKey);
    @file_put_contents($cacheFile, json_encode($data, JSON_PRETTY_PRINT));
    return $data;
}

/* Pick the target file (customize this regex!) */
function pick_target_file(array $files): ?array {
    $matches = [];
    foreach ($files as $f) {
        $name = $f['name'] ?? '';
        if (!$name) continue;

        // CUSTOMIZE THIS PATTERN to match your filenames
        // Example: matches "Policy 2025" or "Policy_2025-26"
        if (preg_match('/policy/i', $name)) {
            $f['_year'] = null;
            if (preg_match('/\b(20\d{2}|19\d{2})(?:-\d{2})?\b/', $name, $m)) {
                $f['_year'] = intval($m[1]);
            }
            $matches[] = $f;
        }
    }

    if (!$matches) return null;

    // Prefer highest year; if tie/none, latest modified
    usort($matches, function($a, $b) {
        $ya = $a['_year'] ?? 0; $yb = $b['_year'] ?? 0;
        if ($ya !== $yb) return $yb <=> $ya;
        return strcmp($b['modifiedTime'] ?? '', $a['modifiedTime'] ?? '');
    });

    return $matches[0] ?? null;
}

/* Download/export to PDF and cache */
function ensure_cached_pdf(array $file, string $apiKey, string $cacheDir, int $ttl): ?string {
    $id   = $file['id']   ?? null;
    $name = $file['name'] ?? ('file_'.$id.'.pdf');
    if (!$id) return null;

    $slug = preg_replace('/[^a-zA-Z0-9._-]+/', '_', $name);
    if (!str_ends_with(strtolower($slug), '.pdf')) $slug .= '.pdf';
    $out = $cacheDir . '/'. $id . '__' . $slug;

    if (is_file($out) && (time() - filemtime($out) < $ttl)) return $out;

    $mime = $file['mimeType'] ?? '';
    if ($mime === 'application/pdf') {
        $downloadUrl = "https://www.googleapis.com/drive/v3/files/{$id}?alt=media&key={$apiKey}";
    } elseif (str_starts_with($mime, 'application/vnd.google-apps.')) {
        $export = $file['exportLinks']['application/pdf'] ?? null;
        $downloadUrl = $export
            ? $export . (str_contains($export, '?') ? '&' : '?') . 'key=' . urlencode($apiKey)
            : "https://www.googleapis.com/drive/v3/files/{$id}/export?mimeType=application/pdf&key={$apiKey}";
    } else {
        $downloadUrl = "https://www.googleapis.com/drive/v3/files/{$id}?alt=media&key={$apiKey}";
    }

    $ctx = stream_context_create([
        'http' => ['method'=>'GET','timeout'=>60,'header'=>"Accept: application/pdf\r\n"]
    ]);
    $tmp = $out . '.part';
    @unlink($tmp);
    $fp = @fopen($tmp, 'wb'); if (!$fp) return null;
    $stream = @fopen($downloadUrl, 'rb', false, $ctx);
    if (!$stream) { @fclose($fp); @unlink($tmp); return null; }
    while (!feof($stream)) {
        $buf = fread($stream, 8192);
        if ($buf === false) break;
        fwrite($fp, $buf);
    }
    fclose($stream); fclose($fp);
    if (!is_file($tmp) || filesize($tmp) < 1024) { @unlink($tmp); return null; }
    @rename($tmp, $out);
    return $out;
}

/* ====== ROUTES ====== */
$forceNoCache = isset($_GET['nocache']) && $_GET['nocache'] == '1';

$files = $forceNoCache
    ? list_drive_children($FOLDER_ID, $GOOGLE_KEY)
    : cached_list($FOLDER_ID, $GOOGLE_KEY, $CACHE_DIR, $LIST_TTL);

if ($forceNoCache) {
    @file_put_contents($CACHE_DIR . '/YOUR_PAGE_NAME_list.json', json_encode($files, JSON_PRETTY_PRINT));
}

if (isset($_GET['debug']) && $_GET['debug'] == '1') {
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($files, JSON_PRETTY_PRINT);
    exit;
}

$chosen = pick_target_file($files);

/* Stream the cached PDF (same-origin for PDF.js) */
if (isset($_GET['pdf']) && $_GET['pdf'] === '1') {
    if (!$chosen) { http_response_code(404); exit('No file found.'); }
    $pdfPath = ensure_cached_pdf($chosen, $GOOGLE_KEY, $CACHE_DIR, $PDF_TTL);
    if (!$pdfPath || !is_file($pdfPath)) { http_response_code(502); exit('Failed to cache PDF.'); }
    header('Content-Type: application/pdf');
    header('Content-Disposition: inline; filename="'.basename($pdfPath).'"');
    header('Content-Length: '.filesize($pdfPath));
    header('Cache-Control: public, max-age=3600');
    readfile($pdfPath);
    exit;
}

/* ====== PAGE ====== */
?><!doctype html>
<meta charset="utf-8">
<title>Your Page Title</title>
<style>
  html, body { margin:0; padding:0; background:#fff; }
  .wrap { width:100%; min-height:100vh; box-sizing:border-box; }
  .pdf-root { width:100%; background:#fff; }
  .page-wrapper { margin:0 auto 24px auto; max-width:1200px; position:relative; }
  canvas.pdf-page { width:100%; height:auto; display:block; background:#fff; }
  .msg { font-family: system-ui, sans-serif; max-width:900px; margin:3rem auto;
         line-height:1.45; color:#222; padding:0 1rem; }
  .msg h1 { font-size:1.4rem; margin:0 0 .5rem; }
  <?php if ($EMBED): ?>
  html, body { height:auto; overflow:visible; }
  <?php endif; ?>
</style>
<div class="wrap">
<?php if (!$chosen): ?>
  <div class="msg">
    <h1>Your Page Title</h1>
    <p>No document found. Try <code>?nocache=1</code>.</p>
  </div>
<?php else:
    $self   = isset($_SERVER['REQUEST_URI']) ? strtok($_SERVER['REQUEST_URI'],'?') : 'your-page.php';
    $pdfUrl = htmlspecialchars($self.'?pdf=1', ENT_QUOTES);
?>
  <div id="pdfRoot" class="pdf-root"></div>
  <script src="/pdfjs/build/pdf.js"></script>
  <script>
    pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdfjs/build/pdf.worker.js';
  </script>
  <script>
  (function(){
    const container = document.getElementById('pdfRoot');
    const status = document.createElement('div');
    status.style.cssText = 'font:14px/1.4 system-ui,Arial; color:#444; margin:16px auto; max-width:1200px;';
    status.textContent = 'Loading PDF…';
    container.appendChild(status);

    let pdfDoc = null;
    const PHP_PDF_URL = <?php echo json_encode($pdfUrl); ?>;
    const IS_EMBED = <?php echo $EMBED ? 'true' : 'false'; ?>;

    function postHeight() {
      if (!IS_EMBED) return;
      const h = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight);
      parent.postMessage({ type: 'yourPageHeight', height: h }, '*');
    }

    async function loadPdf() {
      const resp = await fetch(PHP_PDF_URL, { credentials: 'same-origin' });
      if (!resp.ok) throw new Error('HTTP '+resp.status);
      const buf = await resp.arrayBuffer();
      pdfDoc = await pdfjsLib.getDocument({ data: buf }).promise;
      status.textContent = `Rendering ${pdfDoc.numPages} page(s)…`;
    }

    async function renderAll() {
      if (!pdfDoc) return;
      const maxWidth = Math.min(container.clientWidth || window.innerWidth, 1200);
      const dpr = Math.max(1, window.devicePixelRatio || 1);

      for (let i = 1; i <= pdfDoc.numPages; i++) {
        const page = await pdfDoc.getPage(i);
        const baseVP = page.getViewport({ scale: 1 });
        const layoutScale = maxWidth / Math.ceil(baseVP.width);
        const viewport = page.getViewport({ scale: layoutScale * dpr });

        const wrapper = document.createElement('div');
        wrapper.className = 'page-wrapper';
        wrapper.style.width  = Math.ceil(baseVP.width * layoutScale) + 'px';
        wrapper.style.height = Math.ceil(baseVP.height * layoutScale) + 'px';

        const canvas = document.createElement('canvas');
        canvas.className = 'pdf-page';
        canvas.width  = Math.ceil(viewport.width);
        canvas.height = Math.ceil(viewport.height);
        canvas.style.width  = Math.ceil(viewport.width / dpr) + 'px';
        canvas.style.height = Math.ceil(viewport.height / dpr) + 'px';
        wrapper.appendChild(canvas);
        container.appendChild(wrapper);

        await page.render({ canvasContext: canvas.getContext('2d', {alpha:false}), viewport }).promise;
        if (IS_EMBED) postHeight();
      }
      status.remove();
      postHeight();
    }

    (async () => {
      try {
        await loadPdf();
        await renderAll();
        if (IS_EMBED) {
          window.addEventListener('load', postHeight);
          let t = 0;
          const iv = setInterval(() => { postHeight(); if (++t > 10) clearInterval(iv); }, 400);
        }
      } catch (err) {
        console.error(err);
        status.textContent = 'Failed: ' + (err.message || err);
        status.style.color = '#b00020';
        postHeight();
      }
    })();
  })();
  </script>
<?php endif; ?>
</div>
```

**Steps to implement:**
1. Copy template to `your-page-name.php`
2. Update `$FOLDER_ID` with your Google Drive folder ID
3. Customize the `pick_target_file()` regex pattern
4. Update cache filename: `YOUR_PAGE_NAME_list.json`
5. Update postMessage type: `yourPageHeight`
6. Test with `?debug=1` to see available files
7. Test with `?nocache=1` to force refresh

---

### **Template 2: Static Navigation Menu (HTML)**

Use this for accordion-style link menus.

```html
<!doctype html>
<meta charset="utf-8">
<title>Your Menu Title</title>

<style type="text/css">
.accordion-box {
  width: 80%;
  max-width: 100%;
  margin: 20px 0 0 0;
  box-sizing: border-box;
  font-family: Arial, sans-serif;
  border: 1px solid #c6c6c6;
  border-radius: 10px;
  overflow: hidden;
  box-shadow: 0 2px 6px rgba(0,0,0,0.1);
}

@media (max-width: 768px) {
  .accordion-box {
    width: 100% !important;
    margin: 20px auto 0 auto;
    border-radius: 0;
  }
}

#menu-nav .menu-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px 20px;
  font-size: 18px;
  cursor: pointer;
  border-bottom: 1px solid #ededed;
  user-select: none;
}

#menu-nav .submenu {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.3s ease;
  background: #fafafa;
}

#menu-nav .submenu a {
  display: block;
  padding: 12px 40px;
  font-size: 16px;
  color: #0033FF !important;
  text-decoration: none !important;
  border-bottom: 1px solid #ededed;
}
#menu-nav .submenu a:hover {
  background: #f5f5f5 !important;
  font-weight: 700 !important;
}
</style>

<script>
function toggleAccordion(header){
  var sub = header.nextElementSibling;
  var sym = header.querySelector('span:last-child');
  var box = header.closest('.accordion-box');
  var wasOpen = sub.style.maxHeight && sub.style.maxHeight !== '0px';

  if (box) {
    box.querySelectorAll('.submenu').forEach(s => s.style.maxHeight = '0px');
    box.querySelectorAll('.menu-header span:last-child').forEach(s => s.innerHTML = '+');
  }

  if (!wasOpen){
    sub.style.maxHeight = sub.scrollHeight + 'px';
    sym.innerHTML = '−';
  } else {
    sym.innerHTML = '+';
  }
}

window.addEventListener('resize', function(){
  document.querySelectorAll('#menu-nav .submenu').forEach(s => {
    if (s.style.maxHeight && s.style.maxHeight !== '0px'){
      s.style.maxHeight = s.scrollHeight + 'px';
    }
  });
});
</script>

<div id="menu-nav" class="accordion-box">
  <div class="menu-header" onclick="toggleAccordion(this)">
    <span>Section 1 Name</span>
    <span>+</span>
  </div>
  <div class="submenu">
    <a href="https://example.com/link1" target="_blank">Link 1</a>
    <a href="https://example.com/link2" target="_blank">Link 2</a>
  </div>

  <div class="menu-header" onclick="toggleAccordion(this)">
    <span>Section 2 Name</span>
    <span>+</span>
  </div>
  <div class="submenu">
    <a href="https://example.com/link3" target="_blank">Link 3</a>
  </div>
</div>
```

---

## 🎯 ParentSquare Embed Code

For embedding in ParentSquare CMS:

```html
<p><span style="font-size:48px;"><span style="font-family:Georgia,serif;">Page Title</span></span></p>

<hr class="ckhr" />
<p><iframe
  allow="clipboard-read; clipboard-write"
  id="myFrame"
  loading="lazy"
  src="https://navs.woodsonisd.net/your-page.php?embed=1"
  style="width:100%; border:0; display:block; background:#fff;"
  title="Page Title">
</iframe><script>
  const f = document.getElementById('myFrame');
  function setH(h){ f.style.height = (Math.max(h, 1200)) + 'px'; }
  window.addEventListener('message', (e) => {
    if (!e.data || e.data.type !== 'yourPageHeight') return;
    setH(e.data.height);
  });
  f.addEventListener('load', () => setH(1600));
</script></p>
```

**Important:** Match the `type` in the message listener to your page's postMessage type!

---

## 🛠️ Utilities

### **preload_syllabi.php**
Warms the cache for syllabi by pre-resolving teacher names and course codes.

**Usage:**
```bash
php /var/www/woodson/navs/preload_syllabi.php
```

**When to run:**
- After uploading new syllabi
- Start of school year
- Before high-traffic periods

### **purge_cache.php**
Cleans old cache files to prevent disk space issues.

**Usage:**
```bash
php purge_cache.php --dir=/var/www/woodson/navs/cache --max-age=7d --max-size=300m
```

**Options:**
- `--dir` - Cache directory path
- `--max-age` - Delete files older than (e.g., `7d`, `24h`, `2w`)
- `--max-size` - Total cache size limit (e.g., `300m`, `2g`)
- `--dry-run` - Preview without deleting

**Cron setup** (daily at 2 AM):
```cron
0 2 * * * php /var/www/woodson/navs/purge_cache.php --max-age=7d --max-size=500m
```

---

## 🔍 Troubleshooting

### Page shows "Not Found"
1. Check Drive folder ID is correct
2. Verify API key has Drive API enabled
3. Test with `?debug=1` to see raw file list
4. Check file naming matches regex pattern
5. Try `?nocache=1` to force refresh

### PDF doesn't display
1. Check `/pdfjs/` directory exists and is accessible
2. Verify cache directory is writable: `chmod 775 cache/`
3. Check browser console for JavaScript errors
4. Test `?pdf=1` endpoint directly
5. Verify file is actually a PDF (not corrupted)

### Cache not updating
1. Use `?nocache=1` to force refresh
2. Check cache file timestamps: `ls -lh cache/`
3. Delete specific cache: `rm cache/YOUR_PAGE_list.json`
4. Verify `$LIST_TTL` and `$PDF_TTL` values

### PostMessage height not working
1. Check iframe `id` matches JavaScript
2. Verify `type` in postMessage matches listener
3. Check browser console for CSP errors
4. Ensure `?embed=1` parameter is set

### Drive API quota exceeded
1. Increase cache TTL values
2. Run preload scripts during off-hours
3. Check for infinite loops calling API
4. Review Google Cloud Console quotas

---

## 📊 Page Inventory

### Active Pages (27 total)

**PDF Viewers (5):**
- family-engagement.php
- student-parent-grievances.php
- coi.php
- wellness.php
- dit.php

**Document Tables (6):**
- board-agendas.php
- board-minutes.php
- board-terms.php
- shac-agendas.php
- shac-minutes.php
- budgets.php

**Sheets Tables (1):**
- meals.php

**Complex Systems (4):**
- syllabi.php
- student-handbook.php
- staff-handbook.php
- calendar.php

**Static Menus (4):**
- academic-accountability.html
- finance.html
- health.html
- special-education.html

**Utilities (2):**
- preload_syllabi.php
- purge_cache.php

**Archives (1):**
- calendar.php.bak

---

## 🔐 Security Notes

1. **API Key:** Restricted to `navs.woodsonisd.net` domain
2. **CORS:** Headers configured in Apache (see `.htaccess.pre-cors`)
3. **CSP:** Content Security Policy allows PDF.js and inline scripts
4. **File Access:** Cache directory not directly web-accessible
5. **Input Validation:** All Drive API responses sanitized before display

---

## 📝 Best Practices

### File Naming Conventions
- **Include year:** `Document Name 2025.pdf` or `Name_2025-26.pdf`
- **Use consistent separators:** Spaces or underscores (both work)
- **Descriptive names:** Include keywords matching search patterns
- **Date formats:** `YYYY-MM-DD` or `MM-DD-YYYY` for sorting

### Drive Folder Organization
```
Parent Folder/
├── 2024-25/          # Auto-detected by year
│   ├── Document1.pdf
│   └── Document2.pdf
├── 2025-26/          # Newest folder = active
│   ├── Document1.pdf
│   └── Document2.pdf
└── Archive/
```

### Performance Tips
- Use `?embed=1` for iframe pages (no extra headers)
- Set appropriate cache TTLs (longer = fewer API calls)
- Run preload scripts before peak hours
- Monitor cache directory size
- Use `?compact=1` for mobile-friendly layouts

---

## 📞 Support

**Created:** November 5, 2025
**System:** Woodson ISD Navigation Pages
**Platform:** PHP 8.x, Apache 2.4, Google Drive API v3
**Dependencies:** PDF.js, Google Sheets API

For technical issues, check:
1. Apache error logs: `/var/log/apache2/error.log`
2. Application logs: `/var/www/woodson/navs/logs/`
3. Browser console for JavaScript errors
4. Google Cloud Console for API quotas

---

## 🎓 Learning Resources

- [Google Drive API Documentation](https://developers.google.com/drive/api/v3/about-sdk)
- [PDF.js Documentation](https://mozilla.github.io/pdf.js/)
- [Google Sheets Visualization API](https://developers.google.com/chart/interactive/docs/spreadsheets)

---

**End of README**
