# CDN Fallback Strategy

## Overview

All three frontends (Shop, Vendor, Admin) implement a robust CDN fallback strategy for critical CSS and JavaScript assets. This ensures the application works reliably in:

- ✅ **Offline development** environments
- ✅ **Corporate networks** with restricted CDN access
- ✅ **CDN outages** or performance issues
- ✅ **Air-gapped deployments** without internet access

## Assets with Fallback

The following assets are loaded from CDN with automatic fallback to local copies:

### Core Assets (Always Loaded)

| Asset | CDN Source | Local Fallback |
|-------|-----------|----------------|
| **Tailwind CSS** | `https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css` | `static/shared/css/tailwind.min.css` |
| **Alpine.js** | `https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js` | `static/shared/js/vendor/alpine.min.js` |

### Optional Assets (Loaded On Demand)

| Asset | CDN Source | Local Fallback | Used For |
|-------|-----------|----------------|----------|
| **Chart.js** | `https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js` | `static/shared/js/vendor/chart.umd.min.js` | Charts macros |
| **Flatpickr JS** | `https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js` | `static/shared/js/vendor/flatpickr.min.js` | Datepicker macros |
| **Flatpickr CSS** | `https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.css` | `static/shared/css/vendor/flatpickr.min.css` | Datepicker styling |

## Implementation

### Tailwind CSS Fallback

Tailwind CSS uses the HTML `onerror` attribute to detect CDN failures and switch to the local copy:

```html
<!-- Tailwind CSS with CDN fallback -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
      onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/tailwind.min.css') }}';">
```

**How it works:**
1. Browser attempts to load Tailwind CSS from CDN
2. If CDN fails (network error, timeout, 404), `onerror` event fires
3. Handler sets `onerror=null` to prevent infinite loops
4. Handler updates `href` to local copy path
5. Browser automatically loads local copy

### Alpine.js Fallback

Alpine.js uses dynamic script loading with error handling:

```html
<!-- Alpine.js with CDN fallback -->
<script>
(function() {
    var script = document.createElement('script');
    script.defer = true;
    script.src = 'https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js';

    script.onerror = function() {
        console.warn('Alpine.js CDN failed, loading local copy...');
        var fallbackScript = document.createElement('script');
        fallbackScript.defer = true;
        fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/alpine.min.js") }}';
        document.head.appendChild(fallbackScript);
    };

    document.head.appendChild(script);
})();
</script>
```

**How it works:**
1. IIFE creates a script element dynamically
2. Sets `defer` attribute to match CDN loading behavior
3. Attempts to load Alpine.js from CDN
4. If CDN fails, `onerror` handler triggers
5. Logs warning to console for debugging
6. Creates new script element pointing to local copy
7. Appends fallback script to `<head>`

### Optional Libraries (Chart.js & Flatpickr)

Optional libraries are loaded on-demand using Jinja macros. This keeps page load times fast by only loading what's needed.

**Location:** `app/templates/shared/includes/optional-libs.html`

#### Loading Chart.js

In your page template:

```jinja
{% block chartjs_script %}
{% from 'shared/includes/optional-libs.html' import chartjs_loader %}
{{ chartjs_loader() }}
{% endblock %}
```

**Generated code:**

```html
<script>
(function() {
    var script = document.createElement('script');
    script.src = 'https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js';

    script.onerror = function() {
        console.warn('Chart.js CDN failed, loading local copy...');
        var fallbackScript = document.createElement('script');
        fallbackScript.src = '/static/shared/js/vendor/chart.umd.min.js';
        document.head.appendChild(fallbackScript);
    };

    document.head.appendChild(script);
})();
</script>
```

#### Loading Flatpickr

Flatpickr requires both CSS and JS:

```jinja
{# In head - load CSS #}
{% block flatpickr_css %}
{% from 'shared/includes/optional-libs.html' import flatpickr_css_loader %}
{{ flatpickr_css_loader() }}
{% endblock %}

{# Before page scripts - load JS #}
{% block flatpickr_script %}
{% from 'shared/includes/optional-libs.html' import flatpickr_loader %}
{{ flatpickr_loader() }}
{% endblock %}
```

**Generated CSS code:**

```html
<link rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.css"
      onerror="this.onerror=null; this.href='/static/shared/css/vendor/flatpickr.min.css';">
```

#### Available Blocks in admin/base.html

| Block | Purpose | Location |
|-------|---------|----------|
| `flatpickr_css` | Flatpickr CSS | In `<head>` |
| `chartjs_script` | Chart.js | Before page scripts |
| `flatpickr_script` | Flatpickr JS | Before page scripts |
| `extra_scripts` | Page-specific JS | Last in body |

## File Structure

```
static/
├── shared/
│   ├── css/
│   │   ├── tailwind.min.css              # 2.9M - Tailwind CSS v2.2.19
│   │   └── vendor/
│   │       └── flatpickr.min.css         # 16K - Flatpickr v4.6.13
│   └── js/
│       └── vendor/
│           ├── alpine.min.js             # 44K - Alpine.js v3.13.3
│           ├── chart.umd.min.js          # 205K - Chart.js v4.4.1
│           └── flatpickr.min.js          # 51K - Flatpickr v4.6.13
├── shop/
│   └── css/
│       └── shop.css                      # Shop-specific styles
├── vendor/
│   └── css/
│       └── tailwind.output.css           # Vendor-specific overrides
└── admin/
    └── css/
        └── tailwind.output.css           # Admin-specific overrides

app/templates/shared/
├── includes/
│   └── optional-libs.html                # CDN fallback loaders for Chart.js & Flatpickr
└── macros/
    ├── charts.html                       # Chart.js wrapper macros
    └── datepicker.html                   # Flatpickr wrapper macros
```

## Frontend-Specific Implementations

### Shop Frontend

**Template:** `app/templates/shop/base.html`

```html
{# Lines 41-42: Tailwind CSS fallback #}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
      onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/tailwind.min.css') }}';">

{# Lines 247-263: Alpine.js fallback #}
<script>
(function() {
    var script = document.createElement('script');
    script.defer = true;
    script.src = 'https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js';

    script.onerror = function() {
        console.warn('Alpine.js CDN failed, loading local copy...');
        var fallbackScript = document.createElement('script');
        fallbackScript.defer = true;
        fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/alpine.min.js") }}';
        document.head.appendChild(fallbackScript);
    };

    document.head.appendChild(script);
})();
</script>
```

### Vendor Frontend

**Template:** `app/templates/vendor/base.html`

Same pattern as Shop frontend, with vendor-specific Tailwind overrides loaded after the base:

```html
{# Lines 13-14: Tailwind CSS fallback #}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
      onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/tailwind.min.css') }}';">

{# Line 17: Vendor-specific overrides #}
<link rel="stylesheet" href="{{ url_for('static', path='vendor/css/tailwind.output.css') }}" />

{# Lines 62-78: Alpine.js fallback #}
<!-- Same Alpine.js fallback pattern as Shop -->
```

### Admin Frontend

**Template:** `app/templates/admin/base.html`

Same pattern as Vendor frontend, with admin-specific Tailwind overrides:

```html
{# Lines 13-14: Tailwind CSS fallback #}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
      onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/tailwind.min.css') }}';">

{# Line 17: Admin-specific overrides #}
<link rel="stylesheet" href="{{ url_for('static', path='admin/css/tailwind.output.css') }}" />

{# Lines 62-78: Alpine.js fallback #}
<!-- Same Alpine.js fallback pattern as Shop -->
```

## Development Setup

### Verifying Local Assets

Check that local fallback files exist:

```bash
# From project root
ls -lh static/shared/css/tailwind.min.css
ls -lh static/shared/js/vendor/alpine.min.js
```

Expected output:
```
-rw-r--r-- 1 user user 2.9M static/shared/css/tailwind.min.css
-rw-r--r-- 1 user user  44K static/shared/js/vendor/alpine.min.js
```

### Testing Offline Mode

To test the fallback behavior:

1. **Start the application:**
   ```bash
   uvicorn app.main:app --reload
   ```

2. **Simulate CDN failure:**
   - Use browser DevTools Network tab
   - Add blocking rule for `cdn.jsdelivr.net`
   - Or disconnect from internet

3. **Check console output:**
   ```
   ⚠️ Alpine.js CDN failed, loading local copy...
   ```

4. **Verify fallback loaded:**
   - Open DevTools Network tab
   - Check that `/static/shared/js/vendor/alpine.min.js` was loaded
   - Check that `/static/shared/css/tailwind.min.css` was loaded

### Updating Local Assets

To update Tailwind CSS to a newer version:

```bash
cd static/shared/css
curl -o tailwind.min.css https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css
```

To update Alpine.js to a newer version:

```bash
cd static/shared/js/vendor
curl -o alpine.min.js https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js
```

To update Chart.js:

```bash
cd static/shared/js/vendor
curl -o chart.umd.min.js https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js
```

To update Flatpickr:

```bash
# JavaScript
cd static/shared/js/vendor
curl -o flatpickr.min.js https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js

# CSS
cd static/shared/css/vendor
curl -o flatpickr.min.css https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.css
```

**Important:** Update both the local file AND the CDN URL in the following locations:

| Asset | Update Location |
|-------|-----------------|
| Tailwind CSS | All three `base.html` templates |
| Alpine.js | All three `base.html` templates |
| Chart.js | `shared/includes/optional-libs.html` |
| Flatpickr | `shared/includes/optional-libs.html` |

## Production Deployment

### Verification Checklist

Before deploying to production, verify:

- [ ] Local asset files exist in `static/shared/`
- [ ] Core assets: `tailwind.min.css`, `alpine.min.js`
- [ ] Optional assets: `chart.umd.min.js`, `flatpickr.min.js`, `flatpickr.min.css`
- [ ] File permissions are correct (readable by web server)
- [ ] Static file serving is enabled in FastAPI
- [ ] CDN URLs are accessible from production network
- [ ] Fallback mechanism tested in production-like environment

### Docker Deployment

Ensure local assets are included in Docker image:

```dockerfile
# Dockerfile
COPY static/ /app/static/

# Verify core files exist
RUN test -f /app/static/shared/css/tailwind.min.css && \
    test -f /app/static/shared/js/vendor/alpine.min.js

# Verify optional library files exist
RUN test -f /app/static/shared/js/vendor/chart.umd.min.js && \
    test -f /app/static/shared/js/vendor/flatpickr.min.js && \
    test -f /app/static/shared/css/vendor/flatpickr.min.css
```

### Static File Configuration

FastAPI configuration in `app/main.py`:

```python
from fastapi.staticfiles import StaticFiles

app.mount("/static", StaticFiles(directory="static"), name="static")
```

## Troubleshooting

### Issue: Both CDN and local copy fail to load

**Symptoms:**
- No Tailwind styles applied
- Alpine.js not initializing
- Console errors about missing files

**Solutions:**
1. Check static file mounting configuration
2. Verify file paths in browser Network tab
3. Check file permissions: `chmod 644 static/shared/css/tailwind.min.css`
4. Verify FastAPI StaticFiles mount point

### Issue: Fallback triggers unnecessarily

**Symptoms:**
- Console warnings in normal operation
- Inconsistent CDN loading

**Solutions:**
1. Check network stability
2. Verify CDN URLs are correct and accessible
3. Check for firewall/proxy blocking CDN
4. Consider using local-only mode for restricted networks

### Issue: Page loads slowly due to CDN timeout

**Symptoms:**
- Long wait before fallback triggers
- Poor user experience during CDN issues

**Solutions:**
1. Reduce CDN timeout (browser-dependent)
2. Consider Content Security Policy (CSP) with shorter timeouts
3. For air-gapped deployments, remove CDN URLs entirely and use local-only

## Performance Considerations

### CDN Benefits (Normal Operation)
- ✅ Faster initial load (CDN edge caching)
- ✅ Reduced server bandwidth
- ✅ Browser caching across sites using same CDN

### Fallback Impact
- ⚠️ Additional ~3MB transferred on first load (if CDN fails)
- ⚠️ Slight delay during CDN failure detection
- ✅ Subsequent page loads use browser cache

### Optimization Strategies

1. **For production with reliable CDN access:**
   - Keep current implementation
   - Monitor CDN uptime

2. **For corporate/restricted networks:**
   - Consider removing CDN URLs entirely
   - Load local assets directly

3. **For air-gapped deployments:**
   - Remove CDN URLs completely
   - Update templates to use local-only:

```html
<!-- Local-only mode (no CDN) -->
<link rel="stylesheet" href="{{ url_for('static', path='shared/css/tailwind.min.css') }}">
<script defer src="{{ url_for('static', path='shared/js/vendor/alpine.min.js') }}"></script>
```

## Browser Compatibility

The fallback strategy works in all modern browsers:

| Browser | Tailwind Fallback | Alpine.js Fallback |
|---------|------------------|-------------------|
| Chrome 90+ | ✅ | ✅ |
| Firefox 88+ | ✅ | ✅ |
| Safari 14+ | ✅ | ✅ |
| Edge 90+ | ✅ | ✅ |

**Note:** Internet Explorer is not supported (Alpine.js requires ES6+).

## Security Considerations

### Subresource Integrity (SRI)

Currently not implemented. To add SRI hashes:

```html
<link rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
      integrity="sha384-..."
      crossorigin="anonymous"
      onerror="...">
```

**Trade-off:** SRI hashes prevent loading if CDN file changes, which may interfere with fallback mechanism.

### Content Security Policy (CSP)

If using CSP, ensure CDN domains are whitelisted:

```http
Content-Security-Policy:
  style-src 'self' https://cdn.jsdelivr.net;
  script-src 'self' https://cdn.jsdelivr.net 'unsafe-inline';
```

**Note:** `'unsafe-inline'` is required for the Alpine.js fallback IIFE.

## Related Documentation

- [Shop Frontend Architecture](shop/architecture.md)
- [Vendor Frontend Architecture](vendor/architecture.md)
- [Admin Frontend Architecture](admin/architecture.md)
- [Production Deployment](../deployment/production.md)
- [Docker Deployment](../deployment/docker.md)
- [Troubleshooting Guide](../development/troubleshooting.md)
