tons of stuff but working again

This commit is contained in:
2025-07-10 02:21:19 -05:00
parent 5e828f8e74
commit 2b63f4c9c7
369 changed files with 337 additions and 472 deletions

Binary file not shown.

View File

@ -0,0 +1,3 @@
[Trash Info]
Path=beta-0.1.19.zip
DeletionDate=2025-07-09T23:36:34

View File

@ -2,31 +2,35 @@ FROM python:3.11-slim
ENV PYTHONUNBUFFERED=1
# Install build deps and netcat for the DB-wait
RUN apt-get update && apt-get install -y \
gcc \
default-libmysqlclient-dev \
pkg-config \
netcat-openbsd \
curl \
&& rm -rf /var/lib/apt/lists/*
# 1) Install build deps, netcat, curl—and gosu for privilege dropping
RUN apt-get update \
&& apt-get install -y \
gcc \
default-libmysqlclient-dev \
pkg-config \
netcat-openbsd \
curl \
gosu \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# 2) Copy & install Python requirements
COPY requirements.txt .
RUN pip install --upgrade pip \
&& pip install -r requirements.txt
# 3) Copy the rest of the app
COPY . .
# Create a non-root user and give it ownership of /app
RUN useradd -ms /bin/bash appuser \
&& chown -R appuser:appuser /app
# 4) Create the non-root user and make sure the upload dir exists and is chownd
RUN groupadd -g 1000 appuser \
&& useradd -u 1000 -ms /bin/bash -g appuser appuser \
&& mkdir -p /app/data/uploads \
&& chown -R appuser:appuser /app/data/uploads
# Switch to appuser for everything below
USER appuser
# Prepare entrypoint
COPY --chown=appuser:appuser entrypoint.sh /entrypoint.sh
# 5) Install the entrypoint (keep this as root so it can chown the volume at runtime)
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

View File

@ -5,20 +5,23 @@ DOCKER_COMPOSE=docker compose
PROJECT_NAME=plant_price_tracker
# Commands
.PHONY: help up down rebuild logs status seed shell dbshell reset test
.PHONY: help up down rebuild logs logs-web logs-worker logs-flower logs-all status seed shell dbshell reset test
help:
@echo "Available targets:"
@echo " up - Build and start the app (bootstraps .env if needed)"
@echo " down - Stop and remove containers"
@echo " rebuild - Rebuild containers from scratch"
@echo " logs - Show logs for all services"
@echo " status - Show container health status"
@echo " seed - Manually seed the database"
@echo " shell - Open a bash shell in the web container"
@echo " dbshell - Open a MySQL shell"
@echo " reset - Nuke everything and restart clean"
@echo " test - Run test suite (TBD)"
@echo " up - Build and start the app (bootstraps .env if needed)"
@echo " down - Stop and remove containers"
@echo " rebuild - Rebuild containers from scratch"
@echo " logs-web - Show recent logs for the web service"
@echo " logs-worker - Show recent logs for the Celery worker"
@echo " logs-flower - Show recent logs for the Flower UI"
@echo " logs-all - Show recent logs for all services"
@echo " status - Show container health status"
@echo " seed - Manually seed the database"
@echo " shell - Open a bash shell in the web container"
@echo " dbshell - Open a MySQL shell"
@echo " reset - Nuke everything and restart clean"
@echo " test - Run test suite (TBD)"
up:
@if [ ! -f $(ENV_FILE) ]; then \
@ -51,8 +54,24 @@ rebuild:
preload:
@docker exec -it $$(docker ps -qf "name=$(PROJECT_NAME)-web") flask preload-data
logs:
$(DOCKER_COMPOSE) logs -f
logs-web:
@echo "[📜] Tailing last 200 lines of web service logs…"
$(DOCKER_COMPOSE) logs --tail=200 -f web
logs-worker:
@echo "[📜] Tailing last 200 lines of worker service logs…"
$(DOCKER_COMPOSE) logs --tail=200 -f worker
logs-flower:
@echo "[📜] Tailing last 200 lines of flower service logs…"
$(DOCKER_COMPOSE) logs --tail=200 -f flower
logs-all:
@echo "[📜] Tailing last 200 lines of ALL services logs…"
$(DOCKER_COMPOSE) logs --tail=200 -f
# alias old 'logs' to unified
logs: logs-all
status:
@echo "[📊] Health status of containers:"
@ -100,4 +119,3 @@ migrate:
upgrade:
flask db upgrade

View File

@ -1,260 +1,52 @@
# File: app/__init__.py
import os
import json
import importlib
import time
import logging
from datetime import datetime
from dotenv import load_dotenv, find_dotenv
from flask import Flask, request
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate, upgrade, migrate as _migrate, stamp as _stamp
from flask_login import LoginManager
from flask_wtf.csrf import CSRFProtect
# ─── Core extensions ───────────────────────────────────────────────────────────
db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()
csrf = CSRFProtect()
from .config import Config
from .extensions import db, migrate, login_manager, csrf
from .plugin_loader import register_plugins
def create_app():
# ─── Load .env ───────────────────────────────────────────────────────────────
dotenv_path = find_dotenv()
if dotenv_path:
load_dotenv(dotenv_path, override=True)
# 1) load .env
if p := find_dotenv():
load_dotenv(p, override=True)
# ─── Flask setup ─────────────────────────────────────────────────────────────
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
migrations_dir = os.path.join(project_root, 'migrations')
# 2) create Flask
app = Flask(__name__, static_folder=os.path.join(os.path.dirname(__file__), '..', 'static'))
app.config.from_object(Config)
app = Flask(
__name__,
static_folder=os.path.join(project_root, 'static'),
static_url_path='/static'
)
# install INFOlevel logging handler
# 3) logging
handler = logging.StreamHandler()
handler.setLevel(logging.INFO)
app.logger.setLevel(logging.INFO)
app.logger.addHandler(handler)
app.logger.info("🚀 Starting create_app()")
app.logger.info("🚀 Starting Flask app")
# main config
app.config.from_object('app.config.Config')
app.logger.info(f"🔧 Loaded config from {app.config.__class__.__name__}")
# ─── Init extensions ─────────────────────────────────────────────────────────
# 4) init extensions
csrf.init_app(app)
db.init_app(app)
migrate.init_app(app, db, directory=migrations_dir)
migrate.init_app(app, db)
login_manager.init_app(app)
login_manager.login_view = 'auth.login'
app.logger.info("🔗 Initialized extensions (CSRF, SQLAlchemy, Migrate, LoginManager)")
app.logger.info("🔗 Extensions initialized")
# ─── AUTOMATIC MIGRATIONS ────────────────────────────────────────────────────
with app.app_context():
app.logger.info("🛠️ Checking for schema changes…")
try:
upgrade()
app.logger.info("🛠️ Alembic reports DB is at head")
except Exception:
_stamp(revision='head')
upgrade()
app.logger.info("🛠️ Stamped and upgraded to head")
try:
_migrate(message="autogen", autogenerate=True)
app.logger.info("🛠️ Autogenerated migration revision created")
except Exception:
app.logger.debug("🛠️ No new migrations detected")
upgrade()
app.logger.info("🛠️ Database fully migrated")
# ─── Core routes & errorhandlers ─────────────────────────────────────────────
# 5) register core routes, errors, etc.
from .errors import bp as errors_bp # noqa: E402
app.register_blueprint(errors_bp)
from .routes import init_app as register_core_routes # noqa: E402
register_core_routes(app)
# ─── JSONdriven plugin loader with unbuffered prints ─────────────────────────
print("🔌 Discovering plugins…", flush=True)
plugins_dir = os.path.join(project_root, 'plugins')
loaded_plugins = []
for name in sorted(os.listdir(plugins_dir)):
manifest = os.path.join(plugins_dir, name, 'plugin.json')
if not os.path.isfile(manifest):
continue
plugin_info = {
'name': name,
'models': [],
'routes': None,
'cli': [],
'template_globals': [],
'tasks': [],
'tasks_init': [],
'subplugins': []
}
errors = []
try:
meta = json.load(open(manifest))
except Exception as e:
print(f" ✖ manifest load error for '{name}': {e}", flush=True)
continue
# 1) models
for model_path in meta.get('models', []):
try:
importlib.import_module(model_path)
plugin_info['models'].append(model_path)
except Exception as e:
errors.append(f"model '{model_path}': {e}")
# 2) user_loader
ul = meta.get('user_loader')
if ul:
try:
m = importlib.import_module(ul['module'])
fn = getattr(m, ul['callable'])
fn(login_manager)
except Exception as e:
errors.append(f"user_loader '{ul}': {e}")
# 3) routes
rt = meta.get('routes')
if rt:
try:
m = importlib.import_module(rt['module'])
bp_obj = getattr(m, rt['blueprint'])
prefix = rt.get('url_prefix')
app.register_blueprint(bp_obj, url_prefix=prefix, strict_slashes=False)
plugin_info['routes'] = f"{rt['module']}::{rt['blueprint']}"
except Exception as e:
errors.append(f"routes '{rt}': {e}")
# 4) CLI
cli = meta.get('cli')
if cli:
try:
m = importlib.import_module(cli['module'])
fn = getattr(m, cli['callable'])
app.cli.add_command(fn)
plugin_info['cli'].append(f"{cli['module']}::{cli['callable']}")
except Exception as e:
errors.append(f"cli '{cli}': {e}")
# 5) template_globals
for tg in meta.get('template_globals', []):
try:
mod_name, fn_name = tg['callable'].rsplit('.', 1)
m = importlib.import_module(mod_name)
fn = getattr(m, fn_name)
app.jinja_env.globals[tg['name']] = fn
plugin_info['template_globals'].append(tg['name'])
except Exception as e:
errors.append(f"template_global '{tg}': {e}")
# 6) subplugins
for sp in meta.get('subplugins', []):
sub_info = {'name': sp['name'], 'models': [], 'routes': None}
for mp in sp.get('models', []):
try:
importlib.import_module(mp)
sub_info['models'].append(mp)
except Exception as e:
errors.append(f"subplugin model '{mp}': {e}")
srt = sp.get('routes')
if srt:
try:
m = importlib.import_module(srt['module'])
bp_obj = getattr(m, srt['blueprint'])
app.register_blueprint(bp_obj, url_prefix=srt.get('url_prefix'), strict_slashes=False)
sub_info['routes'] = f"{srt['module']}::{srt['blueprint']}"
except Exception as e:
errors.append(f"subplugin routes '{srt}': {e}")
plugin_info['subplugins'].append(sub_info)
# 7) tasks
for task_mod in meta.get('tasks', []):
try:
importlib.import_module(task_mod)
plugin_info['tasks'].append(task_mod)
except Exception as e:
errors.append(f"task '{task_mod}': {e}")
# 8) tasks_init
for hook in meta.get('tasks_init', []):
try:
m = importlib.import_module(hook['module'])
fn = getattr(m, hook['callable'])
fn(app)
plugin_info['tasks_init'].append(f"{hook['module']}::{hook['callable']}")
except Exception as e:
errors.append(f"tasks_init '{hook}': {e}")
if errors:
print(f" ✖ Plugin '{name}' errors: " + "; ".join(errors), flush=True)
else:
print(f" ✔ Plugin '{name}' loaded", flush=True)
loaded_plugins.append(plugin_info)
# summary
print("🌟 Loaded plugins summary:", flush=True)
for info in loaded_plugins:
print(
f"{info['name']}: "
f"models={info['models']}, "
f"routes={info['routes']}, "
f"cli={info['cli']}, "
f"template_globals={info['template_globals']}, "
f"tasks={info['tasks']}, "
f"tasks_init={info['tasks_init']}, "
f"subplugins={[s['name'] for s in info['subplugins']]}",
flush=True
)
# ─── Context processors, before/after request, teardown ─────────────────────
@app.context_processor
def inject_current_year():
return {'current_year': datetime.now().year}
# 6) register all plugins
register_plugins(app)
# 8) any context-processors, before/after teardown…
@app.context_processor
def inject_now():
return {'utcnow': datetime.utcnow()}
@app.before_request
def start_timer():
request._start_time = time.time()
@app.after_request
def log_analytics(response):
from plugins.admin.models import AnalyticsEvent # noqa: E402
try:
duration = time.time() - getattr(request, '_start_time', time.time())
ev = AnalyticsEvent(
method = request.method,
path = request.path,
status_code = response.status_code,
response_time = duration,
user_agent = request.headers.get('User-Agent'),
referer = request.headers.get('Referer'),
accept_language = request.headers.get('Accept-Language'),
)
db.session.add(ev)
db.session.commit()
except Exception:
db.session.rollback()
return response
@app.teardown_appcontext
def shutdown_session(exception=None):
db.session.remove()
print("✅ create_app() complete; ready to serve", flush=True)
return app

View File

@ -1,12 +1,8 @@
# File: app/celery_app.py
import os
import json
import importlib
from celery import Celery
# 1) Create the Celery object (broker/backend will be set from our Flask config)
celery = Celery('natureinpots')
from app.celery_instance import celery
def init_celery(app):
@ -20,15 +16,18 @@ def init_celery(app):
# 2. Make all tasks run inside the Flask application context
TaskBase = celery.Task
class ContextTask(TaskBase):
def __call__(self, *args, **kwargs):
with app.app_context():
return TaskBase.__call__(self, *args, **kwargs)
celery.Task = ContextTask
# 3. Discover all plugins by looking for plugin.json in plugins/
plugins_dir = os.path.join(app.root_path, '..', 'plugins')
task_modules = []
for plugin_name in sorted(os.listdir(plugins_dir)):
manifest = os.path.join(plugins_dir, plugin_name, 'plugin.json')
if not os.path.isfile(manifest):
@ -76,9 +75,7 @@ def init_celery(app):
return celery
# 5) Immediately bootstrap Celery with our Flask app so that
# any `celery -A app.celery_app:celery worker --beat` invocation
# will pick up your plugins tasks and schedules.
# Immediately bootstrap Celery whenever this module is imported
from app import create_app
_flask_app = create_app()
init_celery(_flask_app)

4
app/celery_instance.py Normal file
View File

@ -0,0 +1,4 @@
from celery import Celery
# Simple, standalone Celery instance
celery = Celery('natureinpots')

11
app/extensions.py Normal file
View File

@ -0,0 +1,11 @@
# File: app/extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_wtf.csrf import CSRFProtect
db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()
csrf = CSRFProtect()

118
app/plugin_loader.py Normal file
View File

@ -0,0 +1,118 @@
import os
import json
import importlib
from flask import current_app
from datetime import datetime
def register_plugins(app):
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
plugins_dir = os.path.join(project_root, 'plugins')
loaded = []
app.logger.info("🔌 Discovering plugins…")
for name in sorted(os.listdir(plugins_dir)):
manifest = os.path.join(plugins_dir, name, 'plugin.json')
if not os.path.isfile(manifest):
continue
errors = []
try:
meta = json.load(open(manifest))
except Exception as e:
app.logger.error(f" ✖ manifest load error for '{name}': {e}")
continue
info = {'name': name, 'models': [], 'routes': None, 'cli': [],
'template_globals': [], 'subplugins': []}
# 1) models
for model_path in meta.get('models', []):
try:
importlib.import_module(model_path)
info['models'].append(model_path)
except Exception as e:
errors.append(f"model '{model_path}': {e}")
# 2) user_loader
ul = meta.get('user_loader')
if ul:
try:
m = importlib.import_module(ul['module'])
fn = getattr(m, ul['callable'])
fn(app.login_manager)
except Exception as e:
errors.append(f"user_loader '{ul}': {e}")
# 3) routes
rt = meta.get('routes')
if rt:
try:
m = importlib.import_module(rt['module'])
bp_obj = getattr(m, rt['blueprint'])
app.register_blueprint(bp_obj, url_prefix=rt.get('url_prefix'), strict_slashes=False)
info['routes'] = f"{rt['module']}::{rt['blueprint']}"
except Exception as e:
errors.append(f"routes '{rt}': {e}")
# 4) CLI
raw_cli = meta.get('cli')
if raw_cli:
# wrap a single dict into a list so we can iterate uniformly
cli_entries = raw_cli if isinstance(raw_cli, list) else [raw_cli]
for cli_entry in cli_entries:
try:
module_path = cli_entry.get('module')
callable_name = cli_entry.get('callable')
m = importlib.import_module(module_path)
fn = getattr(m, callable_name)
app.cli.add_command(fn)
info['cli'].append(f"{module_path}::{callable_name}")
except Exception as e:
errors.append(f"cli '{cli_entry}': {e}")
# 5) template_globals
for tg in meta.get('template_globals', []):
try:
mod_name, fn_name = tg['callable'].rsplit('.', 1)
m = importlib.import_module(mod_name)
fn = getattr(m, fn_name)
app.jinja_env.globals[tg['name']] = fn
info['template_globals'].append(tg['name'])
except Exception as e:
errors.append(f"template_global '{tg}': {e}")
# 6) subplugins
for sp in meta.get('subplugins', []):
sub = {'name': sp['name'], 'models': [], 'routes': None}
for mp in sp.get('models', []):
try:
importlib.import_module(mp)
sub['models'].append(mp)
except Exception as e:
errors.append(f"subplugin model '{mp}': {e}")
srt = sp.get('routes')
if srt:
try:
m = importlib.import_module(srt['module'])
bp_obj = getattr(m, srt['blueprint'])
app.register_blueprint(bp_obj, url_prefix=srt.get('url_prefix'), strict_slashes=False)
sub['routes'] = f"{srt['module']}::{srt['blueprint']}"
except Exception as e:
errors.append(f"subplugin routes '{srt}': {e}")
info['subplugins'].append(sub)
# Report
if errors:
app.logger.error(f" ✖ Plugin '{name}' errors: " + "; ".join(errors))
else:
app.logger.info(f" ✔ Plugin '{name}' loaded")
loaded.append(info)
# summary
app.logger.info("🌟 Loaded plugins summary:")
for p in loaded:
app.logger.info(
f"{p['name']}: models={p['models']}, routes={p['routes']}, "
f"cli={p['cli']}, template_globals={p['template_globals']}, "
f"subplugins={[s['name'] for s in p['subplugins']]}"
)

53
app/task_loader.py Normal file
View File

@ -0,0 +1,53 @@
# File: app/task_loader.py
import os
import json
import importlib
from app.celery_app import celery
def register_tasks(app):
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
plugins_dir = os.path.join(project_root, 'plugins')
task_modules = []
app.logger.info("🔌 Discovering Celery plugins…")
for name in sorted(os.listdir(plugins_dir)):
manifest = os.path.join(plugins_dir, name, 'plugin.json')
if not os.path.isfile(manifest):
continue
try:
meta = json.load(open(manifest))
except Exception as e:
app.logger.error(f"[Celery] Failed to load {name}/plugin.json: {e}")
continue
# a) collect task modules
cfg = meta.get('tasks')
if isinstance(cfg, str):
task_modules.append(cfg)
elif isinstance(cfg, dict) and cfg.get('module'):
task_modules.append(cfg['module'])
elif isinstance(cfg, list):
for entry in cfg:
if isinstance(entry, str):
task_modules.append(entry)
elif isinstance(entry, dict) and entry.get('module'):
task_modules.append(entry['module'])
# b) run tasks_init hooks (now passing Celery, not Flask)
for hook in meta.get('tasks_init', []):
module_name = hook.get('module')
fn_name = hook.get('callable')
if not module_name or not fn_name:
continue
try:
m = importlib.import_module(module_name)
fn = getattr(m, fn_name)
fn(celery)
except Exception as e:
app.logger.error(f"[Celery] tasks_init {name}:{module_name}.{fn_name} failed: {e}")
if task_modules:
celery.autodiscover_tasks(task_modules)
app.logger.info(f"[Celery] Autodiscovered tasks: {task_modules}")

BIN
beta-0.2.0.zip Normal file

Binary file not shown.

BIN
betas/beta-0.1.16.zip Normal file

Binary file not shown.

BIN
betas/beta-0.1.17.zip Normal file

Binary file not shown.

BIN
betas/beta-0.1.18.zip Normal file

Binary file not shown.

BIN
betas/beta-0.1.19.zip Normal file

Binary file not shown.

BIN
betas/beta-0.1.20.zip Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 697 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 657 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1017 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 995 KiB

Some files were not shown because too many files have changed in this diff Show More