Fix HTML rendering for service descriptions, allow zero price for services, improve image_url handling

This commit is contained in:
2026-02-01 22:31:00 -06:00
parent d3cad0e5fa
commit 72f17c8be9
32 changed files with 6958 additions and 414 deletions

258
)') Normal file
View File

@@ -0,0 +1,258 @@
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
Commands marked with * may be preceded by a number, _N.
Notes in parentheses indicate the behavior if _N is given.
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
h H Display this help.
q :q Q :Q ZZ Exit.
---------------------------------------------------------------------------
MMOOVVIINNGG
e ^E j ^N CR * Forward one line (or _N lines).
y ^Y k ^K ^P * Backward one line (or _N lines).
f ^F ^V SPACE * Forward one window (or _N lines).
b ^B ESC-v * Backward one window (or _N lines).
z * Forward one window (and set window to _N).
w * Backward one window (and set window to _N).
ESC-SPACE * Forward one window, but don't stop at end-of-file.
d ^D * Forward one half-window (and set half-window to _N).
u ^U * Backward one half-window (and set half-window to _N).
ESC-) RightArrow * Right one half screen width (or _N positions).
ESC-( LeftArrow * Left one half screen width (or _N positions).
ESC-} ^RightArrow Right to last column displayed.
ESC-{ ^LeftArrow Left to first column.
F Forward forever; like "tail -f".
ESC-F Like F but stop when search pattern is found.
r ^R ^L Repaint screen.
R Repaint screen, discarding buffered input.
---------------------------------------------------
Default "window" is the screen height.
Default "half-window" is half of the screen height.
---------------------------------------------------------------------------
SSEEAARRCCHHIINNGG
/_p_a_t_t_e_r_n * Search forward for (_N-th) matching line.
?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line.
n * Repeat previous search (for _N-th occurrence).
N * Repeat previous search in reverse direction.
ESC-n * Repeat previous search, spanning files.
ESC-N * Repeat previous search, reverse dir. & spanning files.
ESC-u Undo (toggle) search highlighting.
ESC-U Clear search highlighting.
&_p_a_t_t_e_r_n * Display only matching lines.
---------------------------------------------------
A search pattern may begin with one or more of:
^N or ! Search for NON-matching lines.
^E or * Search multiple files (pass thru END OF FILE).
^F or @ Start search at FIRST file (for /) or last file (for ?).
^K Highlight matches, but don't move (KEEP position).
^R Don't use REGULAR EXPRESSIONS.
^W WRAP search if no match found.
---------------------------------------------------------------------------
JJUUMMPPIINNGG
g < ESC-< * Go to first line in file (or line _N).
G > ESC-> * Go to last line in file (or line _N).
p % * Go to beginning of file (or _N percent into file).
t * Go to the (_N-th) next tag.
T * Go to the (_N-th) previous tag.
{ ( [ * Find close bracket } ) ].
} ) ] * Find open bracket { ( [.
ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>.
ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>.
---------------------------------------------------
Each "find close bracket" command goes forward to the close bracket
matching the (_N-th) open bracket in the top line.
Each "find open bracket" command goes backward to the open bracket
matching the (_N-th) close bracket in the bottom line.
m_<_l_e_t_t_e_r_> Mark the current top line with <letter>.
M_<_l_e_t_t_e_r_> Mark the current bottom line with <letter>.
'_<_l_e_t_t_e_r_> Go to a previously marked position.
'' Go to the previous position.
^X^X Same as '.
ESC-M_<_l_e_t_t_e_r_> Clear a mark.
---------------------------------------------------
A mark is any upper-case or lower-case letter.
Certain marks are predefined:
^ means beginning of the file
$ means end of the file
---------------------------------------------------------------------------
CCHHAANNGGIINNGG FFIILLEESS
:e [_f_i_l_e] Examine a new file.
^X^V Same as :e.
:n * Examine the (_N-th) next file from the command line.
:p * Examine the (_N-th) previous file from the command line.
:x * Examine the first (or _N-th) file from the command line.
:d Delete the current file from the command line list.
= ^G :f Print current file name.
---------------------------------------------------------------------------
MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS
-_<_f_l_a_g_> Toggle a command line option [see OPTIONS below].
--_<_n_a_m_e_> Toggle a command line option, by name.
__<_f_l_a_g_> Display the setting of a command line option.
___<_n_a_m_e_> Display the setting of an option, by name.
+_c_m_d Execute the less cmd each time a new file is examined.
!_c_o_m_m_a_n_d Execute the shell command with $SHELL.
|XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command.
s _f_i_l_e Save input to a file.
v Edit the current file with $VISUAL or $EDITOR.
V Print version number of "less".
---------------------------------------------------------------------------
OOPPTTIIOONNSS
Most options may be changed either on the command line,
or from within less by using the - or -- command.
Options may be given in one of two forms: either a single
character preceded by a -, or a name preceded by --.
-? ........ --help
Display help (from command line).
-a ........ --search-skip-screen
Search skips current screen.
-A ........ --SEARCH-SKIP-SCREEN
Search starts just after target line.
-b [_N] .... --buffers=[_N]
Number of buffers.
-B ........ --auto-buffers
Don't automatically allocate buffers for pipes.
-c ........ --clear-screen
Repaint by clearing rather than scrolling.
-d ........ --dumb
Dumb terminal.
-D xx_c_o_l_o_r . --color=xx_c_o_l_o_r
Set screen colors.
-e -E .... --quit-at-eof --QUIT-AT-EOF
Quit at end of file.
-f ........ --force
Force open non-regular files.
-F ........ --quit-if-one-screen
Quit if entire file fits on first screen.
-g ........ --hilite-search
Highlight only last match for searches.
-G ........ --HILITE-SEARCH
Don't highlight any matches for searches.
-h [_N] .... --max-back-scroll=[_N]
Backward scroll limit.
-i ........ --ignore-case
Ignore case in searches that do not contain uppercase.
-I ........ --IGNORE-CASE
Ignore case in all searches.
-j [_N] .... --jump-target=[_N]
Screen position of target lines.
-J ........ --status-column
Display a status column at left edge of screen.
-k [_f_i_l_e] . --lesskey-file=[_f_i_l_e]
Use a lesskey file.
-K ........ --quit-on-intr
Exit less in response to ctrl-C.
-L ........ --no-lessopen
Ignore the LESSOPEN environment variable.
-m -M .... --long-prompt --LONG-PROMPT
Set prompt style.
-n -N .... --line-numbers --LINE-NUMBERS
Don't use line numbers.
-o [_f_i_l_e] . --log-file=[_f_i_l_e]
Copy to log file (standard input only).
-O [_f_i_l_e] . --LOG-FILE=[_f_i_l_e]
Copy to log file (unconditionally overwrite).
-p [_p_a_t_t_e_r_n] --pattern=[_p_a_t_t_e_r_n]
Start at pattern (from command line).
-P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t]
Define new prompt.
-q -Q .... --quiet --QUIET --silent --SILENT
Quiet the terminal bell.
-r -R .... --raw-control-chars --RAW-CONTROL-CHARS
Output "raw" control characters.
-s ........ --squeeze-blank-lines
Squeeze multiple blank lines.
-S ........ --chop-long-lines
Chop (truncate) long lines rather than wrapping.
-t [_t_a_g] .. --tag=[_t_a_g]
Find a tag.
-T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e]
Use an alternate tags file.
-u -U .... --underline-special --UNDERLINE-SPECIAL
Change handling of backspaces.
-V ........ --version
Display the version number of "less".
-w ........ --hilite-unread
Highlight first new line after forward-screen.
-W ........ --HILITE-UNREAD
Highlight first new line after any forward movement.
-x [_N[,...]] --tabs=[_N[,...]]
Set tab stops.
-X ........ --no-init
Don't use termcap init/deinit strings.
-y [_N] .... --max-forw-scroll=[_N]
Forward scroll limit.
-z [_N] .... --window=[_N]
Set size of window.
-" [_c[_c]] . --quotes=[_c[_c]]
Set shell quote characters.
-~ ........ --tilde
Don't display tildes after end of file.
-# [_N] .... --shift=[_N]
Set horizontal scroll amount (0 = one half screen width).
--file-size
Automatically determine the size of the input file.
--follow-name
The F command changes files if the input file is renamed.
--incsearch
Search file as each pattern character is typed in.
--line-num-width=N
Set the width of the -N line number field to N characters.
--mouse
Enable mouse input.
--no-keypad
Don't send termcap keypad init/deinit strings.
--no-histdups
Remove duplicates from command history.
--rscroll=C
Set the character used to mark truncated lines.
--save-marks
Retain marks across invocations of less.
--status-col-width=N
Set the width of the -J status column to N characters.
--use-backslash
Subsequent options use backslash as escape char.
--use-color
Enables colored text.
--wheel-lines=N
Each click of the mouse wheel moves N lines.
---------------------------------------------------------------------------
LLIINNEE EEDDIITTIINNGG
These keys can be used to edit text being entered
on the "command line" at the bottom of the screen.
RightArrow ..................... ESC-l ... Move cursor right one character.
LeftArrow ...................... ESC-h ... Move cursor left one character.
ctrl-RightArrow ESC-RightArrow ESC-w ... Move cursor right one word.
ctrl-LeftArrow ESC-LeftArrow ESC-b ... Move cursor left one word.
HOME ........................... ESC-0 ... Move cursor to start of line.
END ............................ ESC-$ ... Move cursor to end of line.
BACKSPACE ................................ Delete char to left of cursor.
DELETE ......................... ESC-x ... Delete char under cursor.
ctrl-BACKSPACE ESC-BACKSPACE ........... Delete word to left of cursor.
ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor.
ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line.
UpArrow ........................ ESC-k ... Retrieve previous command line.
DownArrow ...................... ESC-j ... Retrieve next command line.
TAB ...................................... Complete filename & cycle.
SHIFT-TAB ...................... ESC-TAB Complete filename & reverse cycle.
ctrl-L ................................... Complete filename, list all.

View File

@@ -3,4 +3,19 @@ DB_NAME="test_database"
CORS_ORIGINS="*"
JWT_SECRET="techzone-super-secret-key-2024-production"
DATABASE_URL="postgresql+asyncpg://techzone_user:techzone_pass@localhost:5432/techzone"
PORT=8181
PORT=8181
# Notification Settings
# SMTP Email Configuration (Gmail example)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
# Admin notification email
ADMIN_EMAIL=admin@prompttech.com
# WhatsApp Notification (CallMeBot API)
# Get API key from: https://www.callmebot.com/blog/free-api-whatsapp-messages/
ADMIN_PHONE=+5016261234
WHATSAPP_API_URL=https://api.callmebot.com/whatsapp.php
WHATSAPP_API_KEY=

64
backend/check_admin.py Normal file
View File

@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""Check and create admin user"""
import asyncio
import sys
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import select
import bcrypt
from datetime import datetime, timezone
sys.path.append('/media/pts/Website/PromptTech_Solution_Site/backend')
from models import User, UserRole
from database import DATABASE_URL
async def check_and_create_admin():
engine = create_async_engine(DATABASE_URL, echo=False)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as session:
# Check for existing admin
result = await session.execute(select(User))
users = result.scalars().all()
print(f"\n📊 Current users in database:")
for user in users:
print(f" - {user.email} (Role: {user.role.value})")
# Check if admin@prompttech.com exists
result = await session.execute(
select(User).where(User.email == "admin@prompttech.com")
)
admin = result.scalar_one_or_none()
if admin:
print(f"\n✅ Admin user already exists: admin@prompttech.com")
print(f" If you can't login, the password should be: admin123")
else:
print(f"\n⚠️ Admin user not found. Creating new admin...")
# Create admin user
email = "admin@prompttech.com"
password = "admin123"
name = "Admin User"
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
admin_user = User(
email=email,
name=name,
password=hashed_password,
role=UserRole.ADMIN,
created_at=datetime.now(timezone.utc)
)
session.add(admin_user)
await session.commit()
print(f"\n✅ Admin user created!")
print(f" Email: {email}")
print(f" Password: {password}")
if __name__ == "__main__":
asyncio.run(check_and_create_admin())

View File

@@ -4,7 +4,7 @@ from sqlalchemy import create_engine
import os
# PostgreSQL connection string
DATABASE_URL = os.environ.get('DATABASE_URL', 'postgresql+asyncpg://techzone_user:techzone_pass@localhost:5432/techzone')
DATABASE_URL = os.environ.get('DATABASE_URL', 'postgresql+asyncpg://prompttech_user:prompttech_pass@localhost:5432/prompttech')
SYNC_DATABASE_URL = DATABASE_URL.replace('+asyncpg', '')
# Async engine for FastAPI

View File

@@ -0,0 +1,20 @@
#!/usr/bin/env python3
"""Drop price check constraints from database"""
import asyncio
from database import async_engine
from sqlalchemy import text
async def drop_constraints():
async with async_engine.begin() as conn:
# Drop services price constraint
result1 = await conn.execute(text('ALTER TABLE services DROP CONSTRAINT IF EXISTS chk_services_price_positive'))
print('Dropped chk_services_price_positive')
# Drop products price constraint
result2 = await conn.execute(text('ALTER TABLE products DROP CONSTRAINT IF EXISTS chk_products_price_positive'))
print('Dropped chk_products_price_positive')
print('Done!')
if __name__ == "__main__":
asyncio.run(drop_constraints())

View File

@@ -0,0 +1,9 @@
#!/bin/bash
# Drop check constraints on services and products tables
# Run with: sudo ./drop_price_constraints.sh
sudo -u postgres psql -d prompttech << 'SQL'
ALTER TABLE services DROP CONSTRAINT IF EXISTS chk_services_price_positive;
ALTER TABLE products DROP CONSTRAINT IF EXISTS chk_products_price_positive;
\echo 'Constraints dropped successfully'
SQL

View File

@@ -217,6 +217,23 @@ class Booking(Base):
service_name = Column(String(255))
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Completion fields
completed_at = Column(DateTime(timezone=True), nullable=True)
diagnosis = Column(Text, nullable=True) # Initial diagnosis/issue description
work_performed = Column(Text, nullable=True) # What was done to fix it
technician_notes = Column(Text, nullable=True) # Internal technician notes
service_cost = Column(Float, nullable=True) # Final cost if different from base price
# Payment fields
paid = Column(Boolean, default=False)
paid_at = Column(DateTime(timezone=True), nullable=True)
# Device information fields
device_model = Column(String(255), nullable=True) # e.g., "Dell Latitude 5520"
serial_number = Column(String(255), nullable=True)
product_number = Column(String(255), nullable=True)
screen_size = Column(String(50), nullable=True) # e.g., "15-inch", "13-inch"
service = relationship("Service", back_populates="bookings")
user = relationship("User", back_populates="bookings")
@@ -300,4 +317,35 @@ class CompanyValue(Base):
display_order = Column(Integer, default=0)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
class MediaType(enum.Enum):
IMAGE = "image"
DOCUMENT = "document"
VIDEO = "video"
OTHER = "other"
class Media(Base):
__tablename__ = "media"
id = Column(String(36), primary_key=True, default=generate_uuid)
filename = Column(String(255), nullable=False)
original_filename = Column(String(255), nullable=False)
file_path = Column(String(500), nullable=False)
file_url = Column(String(500), nullable=False)
file_size = Column(Integer, default=0) # Size in bytes
mime_type = Column(String(100))
media_type = Column(SQLEnum(MediaType), default=MediaType.IMAGE)
alt_text = Column(String(255))
title = Column(String(255))
description = Column(Text)
width = Column(Integer) # For images
height = Column(Integer) # For images
uploaded_by = Column(String(36), ForeignKey("users.id"), nullable=True)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
uploader = relationship("User", foreign_keys=[uploaded_by])

View File

@@ -9,6 +9,7 @@ from sqlalchemy import select, func, and_, or_, desc, asc, distinct, delete
from sqlalchemy.orm import selectinload
import os
import logging
import aiofiles
from pathlib import Path
from pydantic import BaseModel, Field, EmailStr, ConfigDict
from typing import List, Optional, Dict, Any
@@ -20,6 +21,10 @@ import io
import csv
import base64
import shutil
import smtplib
import httpx
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from contextlib import asynccontextmanager
from reportlab.lib import colors
from reportlab.lib.pagesizes import letter, A4
@@ -32,7 +37,7 @@ from models import (
User, Product, Service, CartItem, Order, OrderItem, OrderStatusHistory,
Review, Booking, Contact, InventoryLog, Category, SalesReport,
OrderStatus, UserRole, Base, ProductImage, ServiceImage,
AboutContent, TeamMember, CompanyValue
AboutContent, TeamMember, CompanyValue, Media, MediaType
)
ROOT_DIR = Path(__file__).parent
@@ -42,6 +47,26 @@ load_dotenv(ROOT_DIR / '.env')
UPLOAD_DIR = ROOT_DIR / 'uploads' / 'products'
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
# Notification settings (from environment variables)
SMTP_HOST = os.environ.get('SMTP_HOST', 'smtp.gmail.com')
SMTP_PORT = int(os.environ.get('SMTP_PORT', 587))
SMTP_USER = os.environ.get('SMTP_USER', '')
SMTP_PASSWORD = os.environ.get('SMTP_PASSWORD', '')
ADMIN_EMAIL = os.environ.get('ADMIN_EMAIL', 'admin@prompttech.com')
ADMIN_PHONE = os.environ.get('ADMIN_PHONE', '+5016261234') # WhatsApp number
WHATSAPP_API_URL = os.environ.get('WHATSAPP_API_URL', '') # e.g., CallMeBot or Twilio
WHATSAPP_API_KEY = os.environ.get('WHATSAPP_API_KEY', '')
# Create media uploads directory
MEDIA_UPLOAD_DIR = ROOT_DIR / 'uploads' / 'media'
MEDIA_UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
# Allowed file extensions for media
ALLOWED_IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp', '.ico'}
ALLOWED_DOC_EXTENSIONS = {'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt'}
ALLOWED_VIDEO_EXTENSIONS = {'.mp4', '.webm', '.mov', '.avi'}
MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB
# JWT Configuration
SECRET_KEY = os.environ.get('JWT_SECRET', 'techzone-super-secret-key-2024-production')
ALGORITHM = "HS256"
@@ -431,9 +456,139 @@ def booking_to_dict(booking: Booking) -> dict:
"preferred_date": booking.preferred_date,
"notes": booking.notes,
"status": booking.status,
"created_at": booking.created_at.isoformat() if booking.created_at else None
"created_at": booking.created_at.isoformat() if booking.created_at else None,
"completed_at": booking.completed_at.isoformat() if booking.completed_at else None,
"diagnosis": booking.diagnosis,
"work_performed": booking.work_performed,
"technician_notes": booking.technician_notes,
"service_cost": booking.service_cost,
"paid": booking.paid,
"paid_at": booking.paid_at.isoformat() if booking.paid_at else None,
"device_model": booking.device_model,
"serial_number": booking.serial_number,
"product_number": booking.product_number,
"screen_size": booking.screen_size
}
# Booking completion model
class BookingComplete(BaseModel):
diagnosis: Optional[str] = None
work_performed: Optional[str] = None
technician_notes: Optional[str] = None
service_cost: Optional[float] = None
paid: Optional[bool] = None
device_model: Optional[str] = None
serial_number: Optional[str] = None
product_number: Optional[str] = None
screen_size: Optional[str] = None
# ================== NOTIFICATION FUNCTIONS ==================
async def send_email_notification(subject: str, body: str, to_email: str = None):
"""Send email notification to admin"""
if not SMTP_USER or not SMTP_PASSWORD:
logger.warning("Email notification skipped - SMTP not configured")
return False
try:
to_email = to_email or ADMIN_EMAIL
msg = MIMEMultipart()
msg['From'] = SMTP_USER
msg['To'] = to_email
msg['Subject'] = subject
msg.attach(MIMEText(body, 'html'))
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
server.starttls()
server.login(SMTP_USER, SMTP_PASSWORD)
server.send_message(msg)
logger.info(f"Email notification sent to {to_email}")
return True
except Exception as e:
logger.error(f"Failed to send email: {e}")
return False
async def send_whatsapp_notification(message: str, phone: str = None):
"""Send WhatsApp notification using CallMeBot API or similar service"""
if not WHATSAPP_API_URL or not WHATSAPP_API_KEY:
logger.warning("WhatsApp notification skipped - API not configured")
return False
try:
phone = phone or ADMIN_PHONE
# CallMeBot API format: https://api.callmebot.com/whatsapp.php?phone=PHONE&text=MESSAGE&apikey=KEY
async with httpx.AsyncClient() as client:
params = {
'phone': phone.replace('+', ''),
'text': message,
'apikey': WHATSAPP_API_KEY
}
response = await client.get(WHATSAPP_API_URL, params=params, timeout=10)
if response.status_code == 200:
logger.info(f"WhatsApp notification sent to {phone}")
return True
else:
logger.error(f"WhatsApp API error: {response.status_code}")
return False
except Exception as e:
logger.error(f"Failed to send WhatsApp: {e}")
return False
async def notify_booking(booking_data: dict, service_name: str):
"""Send booking notifications via email and WhatsApp"""
# Format the booking details
booking_date = booking_data.get('preferred_date', 'Not specified')
customer_name = booking_data.get('name', 'Unknown')
customer_email = booking_data.get('email', 'Not provided')
customer_phone = booking_data.get('phone', 'Not provided')
notes = booking_data.get('notes', 'None')
# Email body (HTML)
email_body = f"""
<html>
<body style="font-family: Arial, sans-serif; padding: 20px;">
<h2 style="color: #2563eb;">New Service Booking!</h2>
<p>A customer has booked a service on your website.</p>
<table style="border-collapse: collapse; width: 100%; max-width: 500px;">
<tr><td style="padding: 8px; font-weight: bold; border-bottom: 1px solid #eee;">Service:</td>
<td style="padding: 8px; border-bottom: 1px solid #eee;">{service_name}</td></tr>
<tr><td style="padding: 8px; font-weight: bold; border-bottom: 1px solid #eee;">Customer Name:</td>
<td style="padding: 8px; border-bottom: 1px solid #eee;">{customer_name}</td></tr>
<tr><td style="padding: 8px; font-weight: bold; border-bottom: 1px solid #eee;">Email:</td>
<td style="padding: 8px; border-bottom: 1px solid #eee;">{customer_email}</td></tr>
<tr><td style="padding: 8px; font-weight: bold; border-bottom: 1px solid #eee;">Phone:</td>
<td style="padding: 8px; border-bottom: 1px solid #eee;">{customer_phone}</td></tr>
<tr><td style="padding: 8px; font-weight: bold; border-bottom: 1px solid #eee;">Preferred Date:</td>
<td style="padding: 8px; border-bottom: 1px solid #eee;">{booking_date}</td></tr>
<tr><td style="padding: 8px; font-weight: bold; border-bottom: 1px solid #eee;">Notes:</td>
<td style="padding: 8px; border-bottom: 1px solid #eee;">{notes}</td></tr>
</table>
<p style="margin-top: 20px;">Please reach out to the customer as soon as possible.</p>
<p style="color: #666;">- PromptTech Solutions</p>
</body>
</html>
"""
# WhatsApp message (plain text)
whatsapp_msg = f"""🔔 NEW BOOKING!
Service: {service_name}
Customer: {customer_name}
Phone: {customer_phone}
Email: {customer_email}
Date: {booking_date}
Notes: {notes}
Please reach out to the customer."""
# Send both notifications (don't block on failures)
await send_email_notification(
subject=f"New Booking: {service_name} - {customer_name}",
body=email_body
)
await send_whatsapp_notification(whatsapp_msg)
# ================== AUTH ROUTES ==================
@api_router.post("/auth/register", response_model=TokenResponse)
@@ -586,6 +741,20 @@ async def book_service(
)
db.add(booking)
await db.commit()
# Send notifications to admin (email + WhatsApp)
try:
await notify_booking({
'name': booking_data.name,
'email': booking_data.email,
'phone': booking_data.phone,
'preferred_date': booking_data.preferred_date,
'notes': booking_data.notes
}, service.name)
except Exception as e:
logger.error(f"Failed to send booking notifications: {e}")
# Don't fail the booking if notifications fail
return {"message": "Booking created successfully", "booking_id": booking.id}
# ================== CART ROUTES ==================
@@ -1601,6 +1770,125 @@ async def admin_update_booking_status(booking_id: str, status: str, user: User =
await db.commit()
return {"message": "Booking status updated"}
@api_router.put("/admin/bookings/{booking_id}/complete")
async def admin_complete_booking(
booking_id: str,
completion_data: BookingComplete,
user: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db)
):
"""Mark a booking as completed with diagnosis and work performed details"""
result = await db.execute(select(Booking).where(Booking.id == booking_id))
booking = result.scalar_one_or_none()
if not booking:
raise HTTPException(status_code=404, detail="Booking not found")
booking.status = "completed"
booking.completed_at = datetime.now(timezone.utc)
if completion_data.diagnosis:
booking.diagnosis = completion_data.diagnosis
if completion_data.work_performed:
booking.work_performed = completion_data.work_performed
if completion_data.technician_notes:
booking.technician_notes = completion_data.technician_notes
if completion_data.service_cost is not None:
booking.service_cost = completion_data.service_cost
if completion_data.paid is not None:
booking.paid = completion_data.paid
if completion_data.paid:
booking.paid_at = datetime.now(timezone.utc)
if completion_data.device_model:
booking.device_model = completion_data.device_model
if completion_data.serial_number:
booking.serial_number = completion_data.serial_number
if completion_data.product_number:
booking.product_number = completion_data.product_number
if completion_data.screen_size:
booking.screen_size = completion_data.screen_size
await db.commit()
await db.refresh(booking)
logger.info(f"Booking {booking_id} marked as completed by {user.email}")
return booking_to_dict(booking)
@api_router.get("/admin/bookings/{booking_id}/receipt")
async def admin_get_booking_receipt(
booking_id: str,
user: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db)
):
"""Get booking receipt data for printing"""
result = await db.execute(
select(Booking)
.where(Booking.id == booking_id)
.options(selectinload(Booking.service))
)
booking = result.scalar_one_or_none()
if not booking:
raise HTTPException(status_code=404, detail="Booking not found")
# Get service details
service_price = booking.service.price if booking.service else 0
return {
**booking_to_dict(booking),
"service_base_price": service_price,
"final_cost": booking.service_cost if booking.service_cost else service_price,
"company": {
"name": "PromptTech Solutions",
"address": "Belmopan City, Cayo District, Belize",
"phone": "+501 638-6318",
"email": "prompttechbz@gmail.com"
}
}
@api_router.put("/admin/bookings/{booking_id}/update")
async def admin_update_booking(
booking_id: str,
update_data: BookingComplete,
user: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db)
):
"""Update booking details without marking as complete"""
result = await db.execute(select(Booking).where(Booking.id == booking_id))
booking = result.scalar_one_or_none()
if not booking:
raise HTTPException(status_code=404, detail="Booking not found")
if update_data.diagnosis is not None:
booking.diagnosis = update_data.diagnosis
if update_data.work_performed is not None:
booking.work_performed = update_data.work_performed
if update_data.technician_notes is not None:
booking.technician_notes = update_data.technician_notes
if update_data.service_cost is not None:
booking.service_cost = update_data.service_cost
await db.commit()
await db.refresh(booking)
logger.info(f"Booking {booking_id} updated by {user.email}")
return booking_to_dict(booking)
@api_router.delete("/admin/bookings/{booking_id}")
async def admin_delete_booking(
booking_id: str,
user: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db)
):
"""Delete a booking"""
result = await db.execute(select(Booking).where(Booking.id == booking_id))
booking = result.scalar_one_or_none()
if not booking:
raise HTTPException(status_code=404, detail="Booking not found")
await db.delete(booking)
await db.commit()
logger.info(f"Booking {booking_id} deleted by {user.email}")
return {"message": "Booking deleted successfully"}
# Admin - Users Management
# Admin - Reports
@@ -1642,6 +1930,7 @@ async def admin_get_sales_report(
bookings_result = await db.execute(
select(Booking)
.where(and_(Booking.created_at >= start, Booking.created_at <= end))
.options(selectinload(Booking.service))
)
bookings = bookings_result.scalars().all()
@@ -1661,7 +1950,11 @@ async def admin_get_sales_report(
"orders": 0,
"revenue": 0,
"products_sold": 0,
"order_statuses": {}
"order_statuses": {},
"services_booked": 0,
"services_completed": 0,
"services_paid": 0,
"service_revenue": 0
}
report_data[key]["orders"] += 1
@@ -1671,7 +1964,11 @@ async def admin_get_sales_report(
status = order.status.value if order.status else "unknown"
report_data[key]["order_statuses"][status] = report_data[key]["order_statuses"].get(status, 0) + 1
# Add booking counts
# Add booking counts and service revenue
total_service_revenue = 0
total_services_completed = 0
total_services_paid = 0
for booking in bookings:
if period == "daily":
key = booking.created_at.strftime("%Y-%m-%d")
@@ -1686,10 +1983,26 @@ async def admin_get_sales_report(
"orders": 0,
"revenue": 0,
"products_sold": 0,
"order_statuses": {}
"order_statuses": {},
"services_booked": 0,
"services_completed": 0,
"services_paid": 0,
"service_revenue": 0
}
report_data[key]["services_booked"] = report_data[key].get("services_booked", 0) + 1
if booking.status == "completed":
report_data[key]["services_completed"] = report_data[key].get("services_completed", 0) + 1
total_services_completed += 1
if booking.paid:
report_data[key]["services_paid"] = report_data[key].get("services_paid", 0) + 1
total_services_paid += 1
# Use service_cost if set, otherwise use service base price
cost = booking.service_cost if booking.service_cost else (booking.service.price if booking.service else 0)
report_data[key]["service_revenue"] = report_data[key].get("service_revenue", 0) + cost
total_service_revenue += cost
# Calculate totals
total_orders = len(orders)
@@ -1706,6 +2019,10 @@ async def admin_get_sales_report(
"total_revenue": total_revenue,
"total_products_sold": total_products,
"total_services_booked": total_bookings,
"total_services_completed": total_services_completed,
"total_services_paid": total_services_paid,
"total_service_revenue": total_service_revenue,
"combined_revenue": total_revenue + total_service_revenue,
"average_order_value": total_revenue / total_orders if total_orders > 0 else 0
},
"data": list(report_data.values())
@@ -2321,12 +2638,7 @@ async def admin_update_about_content(
db: AsyncSession = Depends(get_db)
):
"""Update about content section"""
try:
content_uuid = uuid.UUID(content_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid content ID format")
result = await db.execute(select(AboutContent).where(AboutContent.id == content_uuid))
result = await db.execute(select(AboutContent).where(AboutContent.id == content_id))
content = result.scalar_one_or_none()
if not content:
@@ -2374,12 +2686,7 @@ async def admin_delete_about_content(
db: AsyncSession = Depends(get_db)
):
"""Delete about content section"""
try:
content_uuid = uuid.UUID(content_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid content ID format")
result = await db.execute(select(AboutContent).where(AboutContent.id == content_uuid))
result = await db.execute(select(AboutContent).where(AboutContent.id == content_id))
content = result.scalar_one_or_none()
if not content:
@@ -2464,12 +2771,7 @@ async def admin_update_team_member(
db: AsyncSession = Depends(get_db)
):
"""Update team member"""
try:
member_uuid = uuid.UUID(member_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid member ID format")
result = await db.execute(select(TeamMember).where(TeamMember.id == member_uuid))
result = await db.execute(select(TeamMember).where(TeamMember.id == member_id))
member = result.scalar_one_or_none()
if not member:
@@ -2519,12 +2821,7 @@ async def admin_delete_team_member(
db: AsyncSession = Depends(get_db)
):
"""Delete team member"""
try:
member_uuid = uuid.UUID(member_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid member ID format")
result = await db.execute(select(TeamMember).where(TeamMember.id == member_uuid))
result = await db.execute(select(TeamMember).where(TeamMember.id == member_id))
member = result.scalar_one_or_none()
if not member:
@@ -2600,12 +2897,7 @@ async def admin_update_company_value(
db: AsyncSession = Depends(get_db)
):
"""Update company value"""
try:
value_uuid = uuid.UUID(value_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid value ID format")
result = await db.execute(select(CompanyValue).where(CompanyValue.id == value_uuid))
result = await db.execute(select(CompanyValue).where(CompanyValue.id == value_id))
value = result.scalar_one_or_none()
if not value:
@@ -2646,12 +2938,7 @@ async def admin_delete_company_value(
db: AsyncSession = Depends(get_db)
):
"""Delete company value"""
try:
value_uuid = uuid.UUID(value_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid value ID format")
result = await db.execute(select(CompanyValue).where(CompanyValue.id == value_uuid))
result = await db.execute(select(CompanyValue).where(CompanyValue.id == value_id))
value = result.scalar_one_or_none()
if not value:
@@ -2717,6 +3004,385 @@ async def seed_data(db: AsyncSession = Depends(get_db)):
async def root():
return {"message": "TechZone API is running", "version": "2.0.0", "status": "healthy"}
# ============================================
# MEDIA MANAGEMENT ENDPOINTS
# ============================================
def get_media_type(filename: str) -> MediaType:
"""Determine media type from file extension"""
ext = Path(filename).suffix.lower()
if ext in ALLOWED_IMAGE_EXTENSIONS:
return MediaType.IMAGE
elif ext in ALLOWED_DOC_EXTENSIONS:
return MediaType.DOCUMENT
elif ext in ALLOWED_VIDEO_EXTENSIONS:
return MediaType.VIDEO
return MediaType.OTHER
def get_image_dimensions(file_path: Path) -> tuple:
"""Get image dimensions if PIL is available"""
try:
from PIL import Image
with Image.open(file_path) as img:
return img.size
except:
return None, None
@api_router.get("/media")
async def get_all_media(
page: int = Query(1, ge=1),
limit: int = Query(20, ge=1, le=100),
media_type: str = Query(None),
search: str = Query(None),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get all media files with pagination"""
if current_user.role not in [UserRole.ADMIN, UserRole.EMPLOYEE]:
raise HTTPException(status_code=403, detail="Not authorized")
try:
query = select(Media).where(Media.is_active == True)
if media_type:
query = query.where(Media.media_type == MediaType(media_type))
if search:
query = query.where(
or_(
Media.original_filename.ilike(f"%{search}%"),
Media.title.ilike(f"%{search}%"),
Media.alt_text.ilike(f"%{search}%")
)
)
# Get total count
count_query = select(func.count(Media.id)).where(Media.is_active == True)
if media_type:
count_query = count_query.where(Media.media_type == MediaType(media_type))
if search:
count_query = count_query.where(
or_(
Media.original_filename.ilike(f"%{search}%"),
Media.title.ilike(f"%{search}%")
)
)
total_result = await db.execute(count_query)
total = total_result.scalar()
# Apply pagination
offset = (page - 1) * limit
query = query.order_by(Media.created_at.desc()).offset(offset).limit(limit)
result = await db.execute(query)
media_items = result.scalars().all()
return {
"items": [{
"id": m.id,
"filename": m.filename,
"original_filename": m.original_filename,
"file_url": m.file_url,
"file_size": m.file_size,
"mime_type": m.mime_type,
"media_type": m.media_type.value,
"alt_text": m.alt_text,
"title": m.title,
"width": m.width,
"height": m.height,
"created_at": m.created_at.isoformat() if m.created_at else None
} for m in media_items],
"total": total,
"page": page,
"limit": limit,
"pages": (total + limit - 1) // limit
}
except Exception as e:
logger.error(f"Error fetching media: {e}")
raise HTTPException(status_code=500, detail=str(e))
@api_router.post("/media/upload")
async def upload_media(
file: UploadFile = File(...),
alt_text: str = Form(None),
title: str = Form(None),
description: str = Form(None),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Upload a media file"""
if current_user.role not in [UserRole.ADMIN, UserRole.EMPLOYEE]:
raise HTTPException(status_code=403, detail="Not authorized")
try:
# Validate file
if not file.filename:
raise HTTPException(status_code=400, detail="No file provided")
# Check file extension
ext = Path(file.filename).suffix.lower()
all_allowed = ALLOWED_IMAGE_EXTENSIONS | ALLOWED_DOC_EXTENSIONS | ALLOWED_VIDEO_EXTENSIONS
if ext not in all_allowed:
raise HTTPException(
status_code=400,
detail=f"File type not allowed. Allowed: {', '.join(all_allowed)}"
)
# Read file content
content = await file.read()
file_size = len(content)
# Check file size
if file_size > MAX_FILE_SIZE:
raise HTTPException(
status_code=400,
detail=f"File too large. Maximum size: {MAX_FILE_SIZE // (1024*1024)}MB"
)
# Generate unique filename
unique_filename = f"{uuid.uuid4()}{ext}"
file_path = MEDIA_UPLOAD_DIR / unique_filename
# Save file
async with aiofiles.open(file_path, 'wb') as f:
await f.write(content)
# Get image dimensions if applicable
width, height = None, None
if ext in ALLOWED_IMAGE_EXTENSIONS:
width, height = get_image_dimensions(file_path)
# Determine media type
media_type = get_media_type(file.filename)
# Create media record
media = Media(
filename=unique_filename,
original_filename=file.filename,
file_path=str(file_path),
file_url=f"/uploads/media/{unique_filename}",
file_size=file_size,
mime_type=file.content_type,
media_type=media_type,
alt_text=alt_text or file.filename,
title=title or Path(file.filename).stem,
description=description,
width=width,
height=height,
uploaded_by=current_user.id
)
db.add(media)
await db.commit()
await db.refresh(media)
logger.info(f"Media uploaded: {unique_filename} by {current_user.email}")
return {
"id": media.id,
"filename": media.filename,
"original_filename": media.original_filename,
"file_url": media.file_url,
"file_size": media.file_size,
"mime_type": media.mime_type,
"media_type": media.media_type.value,
"alt_text": media.alt_text,
"title": media.title,
"width": media.width,
"height": media.height,
"created_at": media.created_at.isoformat() if media.created_at else None
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error uploading media: {e}")
raise HTTPException(status_code=500, detail=str(e))
@api_router.post("/media/upload-multiple")
async def upload_multiple_media(
files: List[UploadFile] = File(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Upload multiple media files at once"""
if current_user.role not in [UserRole.ADMIN, UserRole.EMPLOYEE]:
raise HTTPException(status_code=403, detail="Not authorized")
uploaded = []
errors = []
for file in files:
try:
if not file.filename:
continue
ext = Path(file.filename).suffix.lower()
all_allowed = ALLOWED_IMAGE_EXTENSIONS | ALLOWED_DOC_EXTENSIONS | ALLOWED_VIDEO_EXTENSIONS
if ext not in all_allowed:
errors.append({"filename": file.filename, "error": "File type not allowed"})
continue
content = await file.read()
file_size = len(content)
if file_size > MAX_FILE_SIZE:
errors.append({"filename": file.filename, "error": "File too large"})
continue
unique_filename = f"{uuid.uuid4()}{ext}"
file_path = MEDIA_UPLOAD_DIR / unique_filename
async with aiofiles.open(file_path, 'wb') as f:
await f.write(content)
width, height = None, None
if ext in ALLOWED_IMAGE_EXTENSIONS:
width, height = get_image_dimensions(file_path)
media_type = get_media_type(file.filename)
media = Media(
filename=unique_filename,
original_filename=file.filename,
file_path=str(file_path),
file_url=f"/uploads/media/{unique_filename}",
file_size=file_size,
mime_type=file.content_type,
media_type=media_type,
alt_text=file.filename,
title=Path(file.filename).stem,
width=width,
height=height,
uploaded_by=current_user.id
)
db.add(media)
uploaded.append({
"filename": file.filename,
"file_url": media.file_url
})
except Exception as e:
errors.append({"filename": file.filename, "error": str(e)})
await db.commit()
return {
"uploaded": uploaded,
"errors": errors,
"total_uploaded": len(uploaded),
"total_errors": len(errors)
}
@api_router.get("/media/{media_id}")
async def get_media(
media_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get a single media item"""
if current_user.role not in [UserRole.ADMIN, UserRole.EMPLOYEE]:
raise HTTPException(status_code=403, detail="Not authorized")
result = await db.execute(select(Media).where(Media.id == media_id))
media = result.scalar_one_or_none()
if not media:
raise HTTPException(status_code=404, detail="Media not found")
return {
"id": media.id,
"filename": media.filename,
"original_filename": media.original_filename,
"file_url": media.file_url,
"file_size": media.file_size,
"mime_type": media.mime_type,
"media_type": media.media_type.value,
"alt_text": media.alt_text,
"title": media.title,
"description": media.description,
"width": media.width,
"height": media.height,
"created_at": media.created_at.isoformat() if media.created_at else None
}
@api_router.put("/media/{media_id}")
async def update_media(
media_id: str,
alt_text: str = Form(None),
title: str = Form(None),
description: str = Form(None),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Update media metadata"""
if current_user.role not in [UserRole.ADMIN, UserRole.EMPLOYEE]:
raise HTTPException(status_code=403, detail="Not authorized")
result = await db.execute(select(Media).where(Media.id == media_id))
media = result.scalar_one_or_none()
if not media:
raise HTTPException(status_code=404, detail="Media not found")
if alt_text is not None:
media.alt_text = alt_text
if title is not None:
media.title = title
if description is not None:
media.description = description
await db.commit()
await db.refresh(media)
return {
"id": media.id,
"filename": media.filename,
"file_url": media.file_url,
"alt_text": media.alt_text,
"title": media.title,
"description": media.description
}
@api_router.delete("/media/{media_id}")
async def delete_media(
media_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Delete a media file"""
if current_user.role != UserRole.ADMIN:
raise HTTPException(status_code=403, detail="Admin access required")
result = await db.execute(select(Media).where(Media.id == media_id))
media = result.scalar_one_or_none()
if not media:
raise HTTPException(status_code=404, detail="Media not found")
# Delete physical file
try:
file_path = Path(media.file_path)
if file_path.exists():
file_path.unlink()
except Exception as e:
logger.warning(f"Could not delete file {media.file_path}: {e}")
# Delete from database
await db.delete(media)
await db.commit()
logger.info(f"Media deleted: {media.filename} by {current_user.email}")
return {"message": "Media deleted successfully"}
@api_router.get("/health")
async def health_check(db: AsyncSession = Depends(get_db)):
"""Comprehensive health check endpoint"""
@@ -2740,20 +3406,22 @@ async def health_check(db: AsyncSession = Depends(get_db)):
"timestamp": datetime.now(timezone.utc).isoformat()
}
# Add CORS middleware BEFORE including routes
app.add_middleware(
CORSMiddleware,
allow_credentials=True,
allow_origins=["http://localhost:5300", "http://localhost:3000", "http://127.0.0.1:5300", "http://prompttech.dynns.com"],
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
allow_headers=["*"],
expose_headers=["*"],
)
# Include the router
app.include_router(api_router)
# Mount static files for uploaded images
app.mount("/uploads", StaticFiles(directory=str(ROOT_DIR / "uploads")), name="uploads")
app.add_middleware(
CORSMiddleware,
allow_credentials=True,
allow_origins=os.environ.get('CORS_ORIGINS', '*').split(','),
allow_methods=["*"],
allow_headers=["*"],
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8181)

View File

@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""
Sync About Page Content to Database
This script updates the about_content table with the correct content
"""
import psycopg2
import json
conn = psycopg2.connect(
host='localhost',
database='prompttech',
user='prompttech_user',
password='prompttech_pass'
)
cur = conn.cursor()
# Update Hero Section
hero_content = "Founded in 2021, PromptTech Solutions has evolved from a small repair shop into a comprehensive tech solutions provider. We're here to guide you through any technology challenge—whether it's laptops, desktops, smartphones, or other devices. With expert service and personalized support, we deliver reliable solutions for all your tech needs."
cur.execute("""
UPDATE about_content
SET title = %s, subtitle = %s, content = %s
WHERE section = 'hero'
""", ("Your Trusted", "Tech Partner", hero_content))
print(f"Updated hero: {cur.rowcount} row(s)")
# Update Stats Section
stats_data = {
"stats": [
{"label": "Happy Customers", "value": "1K+"},
{"label": "Products Sold", "value": "500+"},
{"label": "Repairs Done", "value": "1,500+"},
{"label": "Satisfaction Rate", "value": "90%"}
]
}
cur.execute("""
UPDATE about_content
SET data = %s
WHERE section = 'stats'
""", (json.dumps(stats_data),))
print(f"Updated stats: {cur.rowcount} row(s)")
# Update Story Section
story_content = """<p>PromptTech Solutions started with a simple vision: to make quality tech accessible and provide expert support that customers can trust. What began as a small phone repair shop has evolved into a full-service tech destination.</p>
<p>Our team of certified technicians brings years of combined experience in electronics repair, from smartphones to laptops and everything in between. We've helped thousands of customers bring their devices back to life.</p>
<p>Today, we're proud to offer a curated selection of premium electronics alongside our repair services. Every product we sell meets our high standards for quality, and every repair we do is backed by our satisfaction guarantee.</p>"""
cur.execute("""
UPDATE about_content
SET title = %s, content = %s
WHERE section = 'story'
""", ("Our Story", story_content))
print(f"Updated story: {cur.rowcount} row(s)")
conn.commit()
print("All content synced successfully!")
cur.close()
conn.close()

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env python3
import psycopg2
conn = psycopg2.connect(
host='localhost',
database='prompttech',
user='prompttech_user',
password='prompttech_pass'
)
cur = conn.cursor()
new_content = "Founded in 2021, PromptTech Solutions has evolved from a small repair shop into a comprehensive tech solutions provider. We're here to guide you through any technology challenge—whether it's laptops, desktops, smartphones, or other devices. With expert service and personalized support, we deliver reliable solutions for all your tech needs."
cur.execute("UPDATE about_content SET content = %s WHERE section = 'hero'", (new_content,))
conn.commit()
print(f'Updated {cur.rowcount} row(s)')
cur.close()
conn.close()

View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Update admin email from techzone to prompttech"""
import asyncio
import sys
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
sys.path.append('/media/pts/Website/PromptTech_Solution_Site/backend')
from models import User, UserRole
from database import DATABASE_URL
async def update_admin_email():
engine = create_async_engine(DATABASE_URL, echo=False)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as session:
# Find admin user with old email
result = await session.execute(
select(User).where(User.email == "admin@techzone.com")
)
admin = result.scalar_one_or_none()
if admin:
print(f"✓ Found admin user: {admin.email}")
admin.email = "admin@prompttech.com"
await session.commit()
print(f"✓ Updated admin email to: admin@prompttech.com")
print(f"\nNew credentials:")
print(f" Email: admin@prompttech.com")
print(f" Password: admin123")
else:
print("✗ Admin user with email admin@techzone.com not found")
# Show all users
result = await session.execute(select(User))
users = result.scalars().all()
print(f"\nFound {len(users)} users:")
for user in users:
print(f" - {user.email} ({user.role.value})")
if __name__ == "__main__":
asyncio.run(update_admin_email())

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -1,7 +1,8 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -0,0 +1,640 @@
import React, { useState, useEffect, useCallback } from "react";
import axios from "axios";
import {
Upload,
Image as ImageIcon,
FileText,
Video,
File,
Trash2,
Copy,
Check,
Search,
Grid,
List,
Download,
Edit2,
X,
Loader2,
Filter,
} from "lucide-react";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { Textarea } from "./ui/textarea";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "./ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "./ui/alert-dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "./ui/select";
import { toast } from "sonner";
import { useAuth } from "../context/AuthContext";
const API = `${process.env.REACT_APP_BACKEND_URL}/api`;
const MediaManager = ({ onSelect, selectable = false }) => {
const { token } = useAuth();
const [media, setMedia] = useState([]);
const [loading, setLoading] = useState(true);
const [uploading, setUploading] = useState(false);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [total, setTotal] = useState(0);
const [search, setSearch] = useState("");
const [mediaType, setMediaType] = useState("all");
const [viewMode, setViewMode] = useState("grid");
const [selectedMedia, setSelectedMedia] = useState(null);
const [editDialog, setEditDialog] = useState(false);
const [deleteDialog, setDeleteDialog] = useState(false);
const [mediaToDelete, setMediaToDelete] = useState(null);
const [editForm, setEditForm] = useState({
alt_text: "",
title: "",
description: "",
});
const [copiedId, setCopiedId] = useState(null);
const [dragActive, setDragActive] = useState(false);
const fetchMedia = useCallback(async () => {
try {
setLoading(true);
const params = new URLSearchParams({
page: page.toString(),
limit: "24",
});
if (search) params.append("search", search);
if (mediaType !== "all") params.append("media_type", mediaType);
const response = await axios.get(`${API}/media?${params}`, {
headers: { Authorization: `Bearer ${token}` },
});
setMedia(response.data.items);
setTotalPages(response.data.pages);
setTotal(response.data.total);
} catch (error) {
console.error("Error fetching media:", error);
toast.error("Failed to load media");
} finally {
setLoading(false);
}
}, [token, page, search, mediaType]);
useEffect(() => {
fetchMedia();
}, [fetchMedia]);
const handleUpload = async (files) => {
if (!files || files.length === 0) return;
setUploading(true);
const formData = new FormData();
if (files.length === 1) {
formData.append("file", files[0]);
try {
await axios.post(`${API}/media/upload`, formData, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "multipart/form-data",
},
});
toast.success("File uploaded successfully");
fetchMedia();
} catch (error) {
console.error("Upload error:", error);
toast.error(error.response?.data?.detail || "Upload failed");
}
} else {
// Multiple files
for (const file of files) {
formData.append("files", file);
}
try {
const response = await axios.post(
`${API}/media/upload-multiple`,
formData,
{
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "multipart/form-data",
},
},
);
toast.success(`${response.data.total_uploaded} files uploaded`);
if (response.data.total_errors > 0) {
toast.warning(`${response.data.total_errors} files failed to upload`);
}
fetchMedia();
} catch (error) {
console.error("Upload error:", error);
toast.error("Upload failed");
}
}
setUploading(false);
};
const handleDrag = (e) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setDragActive(true);
} else if (e.type === "dragleave") {
setDragActive(false);
}
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
handleUpload(Array.from(e.dataTransfer.files));
}
};
const handleFileSelect = (e) => {
if (e.target.files && e.target.files.length > 0) {
handleUpload(Array.from(e.target.files));
}
};
const copyUrl = (url) => {
const fullUrl = `${process.env.REACT_APP_BACKEND_URL}${url}`;
navigator.clipboard.writeText(fullUrl);
setCopiedId(url);
toast.success("URL copied to clipboard");
setTimeout(() => setCopiedId(null), 2000);
};
const openEditDialog = (item) => {
setSelectedMedia(item);
setEditForm({
alt_text: item.alt_text || "",
title: item.title || "",
description: item.description || "",
});
setEditDialog(true);
};
const handleUpdate = async () => {
try {
const formData = new FormData();
formData.append("alt_text", editForm.alt_text);
formData.append("title", editForm.title);
formData.append("description", editForm.description);
await axios.put(`${API}/media/${selectedMedia.id}`, formData, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("Media updated");
setEditDialog(false);
fetchMedia();
} catch (error) {
toast.error("Failed to update media");
}
};
const confirmDelete = (item) => {
setMediaToDelete(item);
setDeleteDialog(true);
};
const handleDelete = async () => {
try {
await axios.delete(`${API}/media/${mediaToDelete.id}`, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("Media deleted");
setDeleteDialog(false);
setMediaToDelete(null);
fetchMedia();
} catch (error) {
toast.error("Failed to delete media");
}
};
const getMediaIcon = (type) => {
switch (type) {
case "image":
return <ImageIcon className="h-8 w-8" />;
case "video":
return <Video className="h-8 w-8" />;
case "document":
return <FileText className="h-8 w-8" />;
default:
return <File className="h-8 w-8" />;
}
};
const formatFileSize = (bytes) => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
};
const handleMediaSelect = (item) => {
if (selectable && onSelect) {
onSelect(item);
} else {
setSelectedMedia(item);
}
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col md:flex-row gap-4 items-start md:items-center justify-between">
<div>
<h2 className="text-2xl font-bold font-['Outfit']">Media Library</h2>
<p className="text-muted-foreground">
{total} files Upload and manage your images and documents
</p>
</div>
<div className="flex gap-2">
<Button
variant={viewMode === "grid" ? "default" : "outline"}
size="icon"
onClick={() => setViewMode("grid")}
>
<Grid className="h-4 w-4" />
</Button>
<Button
variant={viewMode === "list" ? "default" : "outline"}
size="icon"
onClick={() => setViewMode("list")}
>
<List className="h-4 w-4" />
</Button>
</div>
</div>
{/* Upload Area */}
<div
className={`border-2 border-dashed rounded-xl p-8 text-center transition-colors ${
dragActive
? "border-primary bg-primary/5"
: "border-border hover:border-primary/50"
}`}
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
>
{uploading ? (
<div className="flex flex-col items-center gap-2">
<Loader2 className="h-10 w-10 animate-spin text-primary" />
<p className="text-muted-foreground">Uploading...</p>
</div>
) : (
<>
<Upload className="h-10 w-10 mx-auto text-muted-foreground mb-4" />
<p className="text-lg font-medium mb-2">Drag and drop files here</p>
<p className="text-sm text-muted-foreground mb-4">
Supports: JPG, PNG, GIF, WebP, SVG, PDF, DOC, MP4 and more
</p>
<label htmlFor="file-upload">
<Button asChild>
<span>
<Upload className="h-4 w-4 mr-2" />
Choose Files
</span>
</Button>
<input
id="file-upload"
type="file"
multiple
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.mp4,.webm,.mov"
className="hidden"
onChange={handleFileSelect}
/>
</label>
</>
)}
</div>
{/* Filters */}
<div className="flex flex-col md:flex-row gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search files..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10"
/>
</div>
<Select value={mediaType} onValueChange={setMediaType}>
<SelectTrigger className="w-full md:w-48">
<Filter className="h-4 w-4 mr-2" />
<SelectValue placeholder="Filter by type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="image">Images</SelectItem>
<SelectItem value="document">Documents</SelectItem>
<SelectItem value="video">Videos</SelectItem>
</SelectContent>
</Select>
</div>
{/* Media Grid/List */}
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
) : media.length === 0 ? (
<div className="text-center py-12">
<ImageIcon className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
<p className="text-lg font-medium">No media files yet</p>
<p className="text-muted-foreground">
Upload your first file to get started
</p>
</div>
) : viewMode === "grid" ? (
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
{media.map((item) => (
<div
key={item.id}
className={`group relative bg-card border border-border rounded-lg overflow-hidden cursor-pointer hover:border-primary transition-colors ${
selectable ? "hover:ring-2 ring-primary" : ""
}`}
onClick={() => handleMediaSelect(item)}
>
<div className="aspect-square bg-muted flex items-center justify-center">
{item.media_type === "image" ? (
<img
src={`${process.env.REACT_APP_BACKEND_URL}${item.file_url}`}
alt={item.alt_text || item.original_filename}
className="w-full h-full object-cover"
/>
) : (
<div className="text-muted-foreground">
{getMediaIcon(item.media_type)}
</div>
)}
</div>
<div className="p-2">
<p className="text-xs truncate font-medium">
{item.original_filename}
</p>
<p className="text-xs text-muted-foreground">
{formatFileSize(item.file_size)}
</p>
</div>
{/* Actions overlay */}
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
<Button
size="icon"
variant="secondary"
onClick={(e) => {
e.stopPropagation();
copyUrl(item.file_url);
}}
>
{copiedId === item.file_url ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
<Button
size="icon"
variant="secondary"
onClick={(e) => {
e.stopPropagation();
openEditDialog(item);
}}
>
<Edit2 className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="destructive"
onClick={(e) => {
e.stopPropagation();
confirmDelete(item);
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
) : (
<div className="space-y-2">
{media.map((item) => (
<div
key={item.id}
className="flex items-center gap-4 p-4 bg-card border border-border rounded-lg hover:border-primary cursor-pointer transition-colors"
onClick={() => handleMediaSelect(item)}
>
<div className="w-16 h-16 bg-muted rounded flex items-center justify-center flex-shrink-0">
{item.media_type === "image" ? (
<img
src={`${process.env.REACT_APP_BACKEND_URL}${item.file_url}`}
alt={item.alt_text}
className="w-full h-full object-cover rounded"
/>
) : (
getMediaIcon(item.media_type)
)}
</div>
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{item.original_filename}</p>
<p className="text-sm text-muted-foreground">
{formatFileSize(item.file_size)} {item.mime_type}
{item.width &&
item.height &&
`${item.width}×${item.height}`}
</p>
</div>
<div className="flex gap-2">
<Button
size="icon"
variant="outline"
onClick={(e) => {
e.stopPropagation();
copyUrl(item.file_url);
}}
>
{copiedId === item.file_url ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
<Button
size="icon"
variant="outline"
onClick={(e) => {
e.stopPropagation();
openEditDialog(item);
}}
>
<Edit2 className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="destructive"
onClick={(e) => {
e.stopPropagation();
confirmDelete(item);
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setPage(Math.max(1, page - 1))}
disabled={page === 1}
>
Previous
</Button>
<span className="text-sm text-muted-foreground">
Page {page} of {totalPages}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setPage(Math.min(totalPages, page + 1))}
disabled={page === totalPages}
>
Next
</Button>
</div>
)}
{/* Edit Dialog */}
<Dialog open={editDialog} onOpenChange={setEditDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Media</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{selectedMedia?.media_type === "image" && (
<div className="aspect-video bg-muted rounded-lg overflow-hidden">
<img
src={`${process.env.REACT_APP_BACKEND_URL}${selectedMedia?.file_url}`}
alt={selectedMedia?.alt_text}
className="w-full h-full object-contain"
/>
</div>
)}
<div className="space-y-2">
<Label>Title</Label>
<Input
value={editForm.title}
onChange={(e) =>
setEditForm({ ...editForm, title: e.target.value })
}
/>
</div>
<div className="space-y-2">
<Label>Alt Text</Label>
<Input
value={editForm.alt_text}
onChange={(e) =>
setEditForm({ ...editForm, alt_text: e.target.value })
}
/>
</div>
<div className="space-y-2">
<Label>Description</Label>
<Textarea
value={editForm.description}
onChange={(e) =>
setEditForm({ ...editForm, description: e.target.value })
}
rows={3}
/>
</div>
<div className="space-y-2">
<Label>URL</Label>
<div className="flex gap-2">
<Input
value={`${process.env.REACT_APP_BACKEND_URL}${selectedMedia?.file_url}`}
readOnly
className="flex-1"
/>
<Button
variant="outline"
onClick={() => copyUrl(selectedMedia?.file_url)}
>
<Copy className="h-4 w-4" />
</Button>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setEditDialog(false)}>
Cancel
</Button>
<Button onClick={handleUpdate}>Save Changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Confirmation */}
<AlertDialog open={deleteDialog} onOpenChange={setDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Media?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete "{mediaToDelete?.original_filename}".
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
};
export default MediaManager;

View File

@@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect } from "react";
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Placeholder from "@tiptap/extension-placeholder";
@@ -95,10 +95,14 @@ const MenuBar = ({ editor }) => {
};
const RichTextEditor = ({
value,
content,
onChange,
placeholder = "Enter description...",
}) => {
// Support both 'value' and 'content' props for flexibility
const initialContent = value || content || "";
const editor = useEditor({
extensions: [
StarterKit,
@@ -106,12 +110,23 @@ const RichTextEditor = ({
placeholder,
}),
],
content,
content: initialContent,
onUpdate: ({ editor }) => {
onChange(editor.getHTML());
},
});
// Update editor content when value/content prop changes externally
useEffect(() => {
if (editor && initialContent !== undefined) {
const currentContent = editor.getHTML();
// Only update if the content is actually different (prevents cursor jump)
if (currentContent !== initialContent && initialContent !== "<p></p>") {
editor.commands.setContent(initialContent);
}
}
}, [editor, initialContent]);
return (
<div
className="border border-border rounded-md overflow-hidden resize-y min-h-[240px] max-h-[600px]"

View File

@@ -49,9 +49,10 @@ const ServiceCard = ({ service }) => {
{service.name}
</h3>
<p className="text-sm text-muted-foreground line-clamp-2">
{service.description}
</p>
<div
className="text-sm text-muted-foreground line-clamp-2 prose prose-sm max-w-none"
dangerouslySetInnerHTML={{ __html: service.description }}
/>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Clock className="h-4 w-4" />
@@ -60,9 +61,13 @@ const ServiceCard = ({ service }) => {
<div className="flex items-center justify-between pt-3 border-t border-border">
<div>
<span className="text-xs text-muted-foreground">Starting from</span>
<span className="text-xs text-muted-foreground">
{service.price > 0 ? "Starting from" : "Price"}
</span>
<p className="text-xl font-bold font-['Outfit']">
${service.price.toFixed(2)}
{service.price > 0
? `$${service.price.toFixed(2)}`
: "Contact for quote"}
</p>
</div>
<Button

View File

@@ -168,25 +168,25 @@ const Footer = () => {
<li className="flex items-start gap-3">
<MapPin className="h-4 w-4 mt-0.5 text-muted-foreground flex-shrink-0" />
<span className="text-sm text-muted-foreground">
123 Tech Street, Silicon Valley, CA 94000
Belmopan City, Belize
</span>
</li>
<li className="flex items-center gap-3">
<Phone className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<a
href="tel:+1234567890"
href="tel:+5016386318"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
+1 (234) 567-890
(501) 638-6318
</a>
</li>
<li className="flex items-center gap-3">
<Mail className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<a
href="mailto:info@prompttechsolutions.com"
href="mailto:prompttechbz@gmail.com"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
info@prompttechsolutions.com
prompttechbz@gmail.com
</a>
</li>
</ul>

View File

@@ -45,12 +45,12 @@ const Navbar = () => {
return (
<header className="sticky top-0 z-50 glass glass-border">
<nav className="max-w-7xl mx-auto px-4 md:px-8">
<nav className="max-w-7xl mx-auto px-4 md:px-8 relative">
<div className="flex items-center justify-between h-16">
{/* Logo */}
<Link
to="/"
className="flex items-center gap-2 group"
className="flex items-center gap-2 group flex-shrink-0"
data-testid="navbar-logo"
>
<img
@@ -63,8 +63,8 @@ const Navbar = () => {
</span>
</Link>
{/* Desktop Navigation */}
<div className="hidden md:flex items-center gap-1">
{/* Desktop Navigation - Centered */}
<div className="hidden md:flex items-center gap-1 absolute left-1/2 transform -translate-x-1/2">
{navLinks.map((link) => (
<Link
key={link.path}

View File

@@ -1,63 +1,107 @@
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { Users, Target, Award, Heart, ArrowRight } from "lucide-react";
import {
Users,
Target,
Award,
Heart,
ArrowRight,
Zap,
Shield,
Star,
Lightbulb,
} from "lucide-react";
import { Button } from "../components/ui/button";
import { Badge } from "../components/ui/badge";
import axios from "axios";
const API = `${process.env.REACT_APP_BACKEND_URL}/api`;
// Icon mapping for company values
const iconMap = {
Target: Target,
Users: Users,
Award: Award,
Heart: Heart,
Zap: Zap,
Shield: Shield,
Star: Star,
Lightbulb: Lightbulb,
};
const About = () => {
const [team, setTeam] = useState([]);
const [values, setValues] = useState([]);
const [content, setContent] = useState({});
const [loading, setLoading] = useState(true);
useEffect(() => {
window.scrollTo(0, 0);
fetchAboutData();
}, []);
const team = [
{
name: "Alex Johnson",
role: "Founder & CEO",
image:
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400",
},
{
name: "Sarah Williams",
role: "Head of Operations",
image:
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400",
},
{
name: "Mike Chen",
role: "Lead Technician",
image:
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400",
},
{
name: "Emily Davis",
role: "Customer Success",
image:
"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=400",
},
];
const fetchAboutData = async () => {
try {
const [teamRes, valuesRes, contentRes] = await Promise.all([
axios.get(`${API}/about/team`),
axios.get(`${API}/about/values`),
axios.get(`${API}/about/content`),
]);
const values = [
{
icon: Target,
title: "Quality First",
desc: "We never compromise on the quality of our products and services.",
},
{
icon: Users,
title: "Customer Focus",
desc: "Your satisfaction is our top priority. We listen and deliver.",
},
{
icon: Award,
title: "Excellence",
desc: "We strive for excellence in everything we do.",
},
{
icon: Heart,
title: "Integrity",
desc: "Honest, transparent, and ethical business practices.",
},
];
setTeam(teamRes.data || []);
setValues(valuesRes.data || []);
// Convert content array to object keyed by section
const contentObj = {};
(contentRes.data || []).forEach((item) => {
contentObj[item.section] = item;
});
setContent(contentObj);
} catch (error) {
console.error("Error fetching about data:", error);
// Use fallback data if API fails
setTeam([]);
setValues([
{
icon: "Target",
title: "Quality First",
description:
"We never compromise on the quality of our products and services.",
},
{
icon: "Users",
title: "Customer Focus",
description:
"Your satisfaction is our top priority. We listen and deliver.",
},
{
icon: "Award",
title: "Excellence",
description: "We strive for excellence in everything we do.",
},
{
icon: "Heart",
title: "Integrity",
description: "Honest, transparent, and ethical business practices.",
},
]);
} finally {
setLoading(false);
}
};
// Get icon component from string name
const getIcon = (iconName) => {
return iconMap[iconName] || Target;
};
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin w-8 h-8 border-2 border-primary border-t-transparent rounded-full" />
</div>
);
}
return (
<div className="min-h-screen">
@@ -70,15 +114,15 @@ const About = () => {
About PromptTech Solutions
</Badge>
<h1 className="text-4xl sm:text-5xl font-bold font-['Outfit'] leading-tight">
Your Trusted
{content.hero?.title || "Your Trusted"}
<br />
<span className="text-muted-foreground">Tech Partner</span>
<span className="text-muted-foreground">
{content.hero?.subtitle || "Tech Partner"}
</span>
</h1>
<p className="text-lg text-muted-foreground leading-relaxed">
Founded in 2020, PromptTech Solutions has grown from a small
repair shop to a comprehensive tech solutions provider. We
combine quality products with expert services to deliver the
best tech experience.
{content.hero?.content ||
"Founded in 2021, PromptTech Solutions has evolved from a small repair shop into a comprehensive tech solutions provider. We're here to guide you through any technology challenge—whether it's laptops, desktops, smartphones, or other devices. With expert service and personalized support, we deliver reliable solutions for all your tech needs."}
</p>
<div className="flex flex-wrap gap-4">
<Link to="/products" data-testid="about-shop-now">
@@ -97,9 +141,12 @@ const About = () => {
<div className="relative">
<img
src="https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800"
alt="Our Team"
className="rounded-2xl shadow-2xl"
src={
content.hero?.image_url ||
"/uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg"
}
alt="Tech Repair Services"
className="rounded-2xl shadow-2xl w-full h-auto max-h-[500px] object-cover"
data-testid="about-hero-image"
/>
{/* Stats Card */}
@@ -118,12 +165,14 @@ const About = () => {
<section className="py-12 bg-muted/30">
<div className="max-w-7xl mx-auto px-4 md:px-8">
<div className="grid grid-cols-2 md:grid-cols-4 gap-8">
{[
{ value: "50K+", label: "Happy Customers" },
{ value: "10K+", label: "Products Sold" },
{ value: "15K+", label: "Repairs Done" },
{ value: "98%", label: "Satisfaction Rate" },
].map((stat, idx) => (
{(
content.stats?.data?.stats || [
{ value: "1K+", label: "Happy Customers" },
{ value: "500+", label: "Products Sold" },
{ value: "1,500+", label: "Repairs Done" },
{ value: "90%", label: "Satisfaction Rate" },
]
).map((stat, idx) => (
<div
key={idx}
className="text-center"
@@ -144,28 +193,37 @@ const About = () => {
<div className="max-w-4xl mx-auto px-4 md:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold font-['Outfit'] mb-4">
Our Story
{content.story?.title || "Our Story"}
</h2>
</div>
<div className="prose prose-lg dark:prose-invert max-w-none">
<p className="text-muted-foreground leading-relaxed mb-6">
PromptTech Solutions started with a simple vision: to make quality
tech accessible and provide expert support that customers can
trust. What began as a small phone repair shop has evolved into a
full-service tech destination.
</p>
<p className="text-muted-foreground leading-relaxed mb-6">
Our team of certified technicians brings decades of combined
experience in electronics repair, from smartphones to laptops and
everything in between. We've helped thousands of customers bring
their devices back to life.
</p>
<p className="text-muted-foreground leading-relaxed">
Today, we're proud to offer a curated selection of premium
electronics alongside our repair services. Every product we sell
meets our high standards for quality, and every repair we do is
backed by our satisfaction guarantee.
</p>
{content.story?.content ? (
<div
className="text-muted-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: content.story.content }}
/>
) : (
<>
<p className="text-muted-foreground leading-relaxed mb-6">
PromptTech Solutions started with a simple vision: to make
quality tech accessible and provide expert support that
customers can trust. What began as a small phone repair shop
has evolved into a full-service tech destination.
</p>
<p className="text-muted-foreground leading-relaxed mb-6">
Our team of certified technicians brings decades of combined
experience in electronics repair, from smartphones to laptops
and everything in between. We've helped thousands of customers
bring their devices back to life.
</p>
<p className="text-muted-foreground leading-relaxed">
Today, we're proud to offer a curated selection of premium
electronics alongside our repair services. Every product we
sell meets our high standards for quality, and every repair we
do is backed by our satisfaction guarantee.
</p>
</>
)}
</div>
</div>
</section>
@@ -183,61 +241,115 @@ const About = () => {
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{values.map((value, idx) => {
const Icon = value.icon;
return (
<div
key={idx}
className="p-6 rounded-2xl bg-card border border-border hover-lift text-center"
data-testid={`value-${idx}`}
>
<div className="w-14 h-14 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center">
<Icon className="h-7 w-7 text-primary" />
</div>
<h3 className="text-lg font-semibold mb-2 font-['Outfit']">
{value.title}
</h3>
<p className="text-sm text-muted-foreground">{value.desc}</p>
</div>
);
})}
{values.length > 0
? values.map((value, idx) => {
const Icon = getIcon(value.icon);
return (
<div
key={value.id || idx}
className="p-6 rounded-2xl bg-card border border-border hover-lift text-center"
data-testid={`value-${idx}`}
>
<div className="w-14 h-14 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center">
<Icon className="h-7 w-7 text-primary" />
</div>
<h3 className="text-lg font-semibold mb-2 font-['Outfit']">
{value.title}
</h3>
<p className="text-sm text-muted-foreground">
{value.description}
</p>
</div>
);
})
: // Fallback values
[
{
icon: Target,
title: "Quality First",
desc: "We never compromise on the quality of our products and services.",
},
{
icon: Users,
title: "Customer Focus",
desc: "Your satisfaction is our top priority. We listen and deliver.",
},
{
icon: Award,
title: "Excellence",
desc: "We strive for excellence in everything we do.",
},
{
icon: Heart,
title: "Integrity",
desc: "Honest, transparent, and ethical business practices.",
},
].map((value, idx) => {
const Icon = value.icon;
return (
<div
key={idx}
className="p-6 rounded-2xl bg-card border border-border hover-lift text-center"
data-testid={`value-${idx}`}
>
<div className="w-14 h-14 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center">
<Icon className="h-7 w-7 text-primary" />
</div>
<h3 className="text-lg font-semibold mb-2 font-['Outfit']">
{value.title}
</h3>
<p className="text-sm text-muted-foreground">
{value.desc}
</p>
</div>
);
})}
</div>
</div>
</section>
{/* Team */}
<section className="py-16 md:py-24">
<div className="max-w-7xl mx-auto px-4 md:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold font-['Outfit'] mb-4">
Meet Our Team
</h2>
<p className="text-muted-foreground max-w-2xl mx-auto">
The people behind PromptTech Solutions' success
</p>
</div>
{team.length > 0 && (
<section className="py-16 md:py-24">
<div className="max-w-7xl mx-auto px-4 md:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold font-['Outfit'] mb-4">
Meet Our Team
</h2>
<p className="text-muted-foreground max-w-2xl mx-auto">
The people behind PromptTech Solutions' success
</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
{team.map((member, idx) => (
<div
key={idx}
className="group text-center"
data-testid={`team-member-${idx}`}
>
<div className="relative mb-4 overflow-hidden rounded-2xl aspect-square">
<img
src={member.image}
alt={member.name}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
/>
<div
className={`grid grid-cols-2 ${team.length <= 3 ? "md:grid-cols-3" : "md:grid-cols-4"} gap-6`}
>
{team.map((member, idx) => (
<div
key={member.id || idx}
className="group text-center"
data-testid={`team-member-${idx}`}
>
<div className="relative mb-4 overflow-hidden rounded-2xl aspect-square">
<img
src={
member.image_url ||
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400"
}
alt={member.name}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
/>
</div>
<h3 className="font-semibold font-['Outfit']">
{member.name}
</h3>
<p className="text-sm text-muted-foreground">{member.role}</p>
</div>
<h3 className="font-semibold font-['Outfit']">{member.name}</h3>
<p className="text-sm text-muted-foreground">{member.role}</p>
</div>
))}
))}
</div>
</div>
</div>
</section>
</section>
)}
{/* CTA */}
<section className="py-16 md:py-24 bg-primary text-primary-foreground">

File diff suppressed because it is too large Load Diff

View File

@@ -41,19 +41,19 @@ const Contact = () => {
{
icon: MapPin,
title: "Address",
content: "123 Tech Street, Silicon Valley, CA 94000",
content: "Belmopan City, Belize",
},
{
icon: Phone,
title: "Phone",
content: "+1 (234) 567-890",
link: "tel:+1234567890",
content: "(501) 638-6318",
link: "tel:+5016386318",
},
{
icon: Mail,
title: "Email",
content: "info@prompttechsolutions.com",
link: "mailto:info@prompttechsolutions.com",
content: "prompttechbz@gmail.com",
link: "mailto:prompttechbz@gmail.com",
},
{
icon: Clock,
@@ -113,14 +113,32 @@ const Contact = () => {
);
})}
{/* Map placeholder */}
<div className="aspect-video rounded-xl overflow-hidden border border-border bg-muted">
<img
src="https://images.unsplash.com/photo-1526778548025-fa2f459cd5c1?w=800"
alt="Location Map"
className="w-full h-full object-cover"
data-testid="contact-map"
{/* Google Map - Belmopan City, Belize */}
<div
className="aspect-video rounded-xl overflow-hidden border border-border bg-muted relative group"
data-testid="contact-map"
>
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d30389.40536428!2d-88.7772!3d17.2514!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x8f5d0e6c6c6d4e6f%3A0x9f0b8c9e9e9e9e9e!2sBelmopan%2C%20Belize!5e0!3m2!1sen!2sus!4v1706824800000!5m2!1sen!2sus"
width="100%"
height="100%"
style={{ border: 0 }}
allowFullScreen=""
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
title="PromptTech Solutions Location - Belmopan City, Belize"
className="w-full h-full"
/>
{/* Ctrl + Scroll overlay */}
<div
className="absolute inset-0 bg-black/50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"
style={{ pointerEvents: "none" }}
>
<div className="bg-background/90 px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2">
<kbd className="px-2 py-1 bg-muted rounded text-xs">Ctrl</kbd>
<span>+ scroll to zoom</span>
</div>
</div>
</div>
</div>

View File

@@ -1,21 +1,29 @@
import React, { useState, useEffect } from 'react';
import { useParams, Link, useNavigate } from 'react-router-dom';
import axios from 'axios';
import { ArrowLeft, Clock, Calendar, Check, Phone, Mail, User } from 'lucide-react';
import { Button } from '../components/ui/button';
import { Badge } from '../components/ui/badge';
import { Input } from '../components/ui/input';
import { Textarea } from '../components/ui/textarea';
import { Label } from '../components/ui/label';
import { Separator } from '../components/ui/separator';
import React, { useState, useEffect } from "react";
import { useParams, Link, useNavigate } from "react-router-dom";
import axios from "axios";
import {
ArrowLeft,
Clock,
Calendar,
Check,
Phone,
Mail,
User,
} from "lucide-react";
import { Button } from "../components/ui/button";
import { Badge } from "../components/ui/badge";
import { Input } from "../components/ui/input";
import { Textarea } from "../components/ui/textarea";
import { Label } from "../components/ui/label";
import { Separator } from "../components/ui/separator";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '../components/ui/dialog';
import { toast } from 'sonner';
} from "../components/ui/dialog";
import { toast } from "sonner";
const API = `${process.env.REACT_APP_BACKEND_URL}/api`;
@@ -27,11 +35,11 @@ const ServiceDetail = () => {
const [bookingOpen, setBookingOpen] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
preferred_date: '',
notes: ''
name: "",
email: "",
phone: "",
preferred_date: "",
notes: "",
});
useEffect(() => {
@@ -40,7 +48,7 @@ const ServiceDetail = () => {
const response = await axios.get(`${API}/services/${id}`);
setService(response.data);
} catch (error) {
console.error('Failed to fetch service:', error);
console.error("Failed to fetch service:", error);
} finally {
setLoading(false);
}
@@ -55,13 +63,21 @@ const ServiceDetail = () => {
try {
await axios.post(`${API}/services/book`, {
service_id: service.id,
...formData
...formData,
});
toast.success('Booking submitted successfully! We will contact you soon.');
toast.success(
"Booking submitted successfully! We will contact you soon.",
);
setBookingOpen(false);
setFormData({ name: '', email: '', phone: '', preferred_date: '', notes: '' });
setFormData({
name: "",
email: "",
phone: "",
preferred_date: "",
notes: "",
});
} catch (error) {
toast.error('Failed to submit booking. Please try again.');
toast.error("Failed to submit booking. Please try again.");
} finally {
setSubmitting(false);
}
@@ -85,7 +101,9 @@ const ServiceDetail = () => {
return (
<div className="min-h-screen py-8 md:py-12">
<div className="max-w-4xl mx-auto px-4 md:px-8 text-center py-16">
<h2 className="text-2xl font-bold mb-4 font-['Outfit']">Service not found</h2>
<h2 className="text-2xl font-bold mb-4 font-['Outfit']">
Service not found
</h2>
<Link to="/services">
<Button className="rounded-full">Back to Services</Button>
</Link>
@@ -124,26 +142,33 @@ const ServiceDetail = () => {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2 space-y-6">
<div>
<h1 className="text-3xl md:text-4xl font-bold font-['Outfit'] mb-4" data-testid="service-title">
<h1
className="text-3xl md:text-4xl font-bold font-['Outfit'] mb-4"
data-testid="service-title"
>
{service.name}
</h1>
<p className="text-lg text-muted-foreground leading-relaxed" data-testid="service-description">
{service.description}
</p>
<div
className="text-lg text-muted-foreground leading-relaxed prose prose-lg max-w-none"
data-testid="service-description"
dangerouslySetInnerHTML={{ __html: service.description }}
/>
</div>
<Separator />
{/* What's Included */}
<div>
<h3 className="text-xl font-semibold mb-4 font-['Outfit']">What's Included</h3>
<h3 className="text-xl font-semibold mb-4 font-['Outfit']">
What's Included
</h3>
<ul className="space-y-3">
{[
'Free diagnostic assessment',
'Quality replacement parts (if needed)',
'Professional service by certified technicians',
'30-day warranty on all repairs',
'Post-service support'
"Free diagnostic assessment",
"Quality replacement parts (if needed)",
"Professional service by certified technicians",
"30-day warranty on all repairs",
"Post-service support",
].map((item, idx) => (
<li key={idx} className="flex items-center gap-3">
<div className="w-5 h-5 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
@@ -159,18 +184,37 @@ const ServiceDetail = () => {
{/* Process */}
<div>
<h3 className="text-xl font-semibold mb-4 font-['Outfit']">How It Works</h3>
<h3 className="text-xl font-semibold mb-4 font-['Outfit']">
How It Works
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{[
{ step: '1', title: 'Book', desc: 'Schedule your appointment online' },
{ step: '2', title: 'Drop Off', desc: 'Bring your device to our store' },
{ step: '3', title: 'Pick Up', desc: 'Get your device back fixed' }
{
step: "1",
title: "Book",
desc: "Schedule your appointment online",
},
{
step: "2",
title: "Drop Off",
desc: "Bring your device to our store",
},
{
step: "3",
title: "Pick Up",
desc: "Get your device back fixed",
},
].map((item, idx) => (
<div key={idx} className="text-center p-4 rounded-xl bg-muted/50">
<div
key={idx}
className="text-center p-4 rounded-xl bg-muted/50"
>
<div className="w-10 h-10 mx-auto mb-3 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-bold">
{item.step}
</div>
<h4 className="font-semibold mb-1 font-['Outfit']">{item.title}</h4>
<h4 className="font-semibold mb-1 font-['Outfit']">
{item.title}
</h4>
<p className="text-sm text-muted-foreground">{item.desc}</p>
</div>
))}
@@ -182,9 +226,16 @@ const ServiceDetail = () => {
<div className="lg:col-span-1">
<div className="sticky top-24 border border-border rounded-2xl p-6 bg-card space-y-6">
<div>
<span className="text-sm text-muted-foreground">Starting from</span>
<p className="text-3xl font-bold font-['Outfit']" data-testid="service-price">
${service.price.toFixed(2)}
<span className="text-sm text-muted-foreground">
{service.price > 0 ? "Starting from" : "Price"}
</span>
<p
className="text-3xl font-bold font-['Outfit']"
data-testid="service-price"
>
{service.price > 0
? `$${service.price.toFixed(2)}`
: "Contact for quote"}
</p>
</div>
@@ -195,13 +246,19 @@ const ServiceDetail = () => {
<Dialog open={bookingOpen} onOpenChange={setBookingOpen}>
<DialogTrigger asChild>
<Button className="w-full rounded-full btn-press" size="lg" data-testid="book-now-button">
<Button
className="w-full rounded-full btn-press"
size="lg"
data-testid="book-now-button"
>
Book Now
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="font-['Outfit']">Book {service.name}</DialogTitle>
<DialogTitle className="font-['Outfit']">
Book {service.name}
</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4 mt-4">
<div className="space-y-2">
@@ -213,7 +270,9 @@ const ServiceDetail = () => {
placeholder="John Doe"
className="pl-10"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
required
data-testid="booking-name"
/>
@@ -230,7 +289,9 @@ const ServiceDetail = () => {
placeholder="john@example.com"
className="pl-10"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
onChange={(e) =>
setFormData({ ...formData, email: e.target.value })
}
required
data-testid="booking-email"
/>
@@ -247,7 +308,9 @@ const ServiceDetail = () => {
placeholder="+1 234 567 890"
className="pl-10"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
onChange={(e) =>
setFormData({ ...formData, phone: e.target.value })
}
required
data-testid="booking-phone"
/>
@@ -263,7 +326,12 @@ const ServiceDetail = () => {
type="date"
className="pl-10"
value={formData.preferred_date}
onChange={(e) => setFormData({ ...formData, preferred_date: e.target.value })}
onChange={(e) =>
setFormData({
...formData,
preferred_date: e.target.value,
})
}
required
data-testid="booking-date"
/>
@@ -276,18 +344,20 @@ const ServiceDetail = () => {
id="notes"
placeholder="Describe your issue..."
value={formData.notes}
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
onChange={(e) =>
setFormData({ ...formData, notes: e.target.value })
}
data-testid="booking-notes"
/>
</div>
<Button
type="submit"
className="w-full rounded-full"
<Button
type="submit"
className="w-full rounded-full"
disabled={submitting}
data-testid="submit-booking"
>
{submitting ? 'Submitting...' : 'Submit Booking'}
{submitting ? "Submitting..." : "Submit Booking"}
</Button>
</form>
</DialogContent>

View File

@@ -63,7 +63,6 @@ const Services = () => {
};
const stats = [
{ value: "10K+", label: "Devices Repaired" },
{ value: "98%", label: "Success Rate" },
{ value: "24h", label: "Avg Turnaround" },
{ value: "5 Star", label: "Customer Rating" },
@@ -85,12 +84,14 @@ const Services = () => {
<span className="text-muted-foreground">Tech Solutions</span>
</h1>
<p className="text-lg text-muted-foreground max-w-lg">
From screen repairs to data recovery, our certified technicians
provide professional solutions for all your tech needs.
From screen repairs to advanced data recovery, we provide
reliable and professional solutions for all your technology
needs. Our services cover both hardware and software, ensuring
your devices run smoothly, securely, and efficiently.
</p>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 pt-4">
<div className="grid grid-cols-3 gap-4 pt-4">
{stats.map((stat, idx) => (
<div
key={idx}
@@ -119,6 +120,57 @@ const Services = () => {
</div>
</section>
{/* Specializations Section */}
<section className="py-16 bg-card border-b border-border">
<div className="max-w-7xl mx-auto px-4 md:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
<div>
<h2 className="text-2xl md:text-3xl font-bold font-['Outfit'] mb-6">
We Specialize In
</h2>
<ul className="space-y-3">
{[
"Computer and laptop repairs",
"Screen replacements and hardware troubleshooting",
"Software installation and system configuration",
"Windows 10 and Windows 11 repair, recovery, and optimization",
"Operating system reinstallation and OS-level repairs",
].map((item, idx) => (
<li key={idx} className="flex items-start gap-3">
<div className="w-2 h-2 rounded-full bg-primary mt-2 flex-shrink-0" />
<span className="text-muted-foreground">{item}</span>
</li>
))}
</ul>
</div>
<div>
<h2 className="text-2xl md:text-3xl font-bold font-['Outfit'] mb-6 lg:invisible">
&nbsp;
</h2>
<ul className="space-y-3">
{[
"Microsoft Office installation, activation, and setup",
"Adobe software installation and configuration",
"QuickBooks POS 2019 installation, setup, and troubleshooting",
"Server management and system synchronization across multiple devices",
"Data backup, recovery, and system cleanup",
].map((item, idx) => (
<li key={idx} className="flex items-start gap-3">
<div className="w-2 h-2 rounded-full bg-primary mt-2 flex-shrink-0" />
<span className="text-muted-foreground">{item}</span>
</li>
))}
</ul>
</div>
</div>
<p className="text-center text-muted-foreground mt-8 max-w-3xl mx-auto">
Our certified technicians provide professional solutions for all
your tech needswhether it's a single device repair or managing
systems across your entire business.
</p>
</div>
</section>
{/* Categories */}
<section className="py-8 border-b border-border sticky top-16 bg-background/95 backdrop-blur-sm z-40">
<div className="max-w-7xl mx-auto px-4 md:px-8">

File diff suppressed because it is too large Load Diff

View File

@@ -458,3 +458,760 @@
2026-01-13 18:39:43 -06:00: INFO: 127.0.0.1:43056 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-01-13 18:39:49 -06:00: INFO: 127.0.0.1:43062 - "OPTIONS /api/admin/inventory HTTP/1.1" 200 OK
2026-01-13 18:39:49 -06:00: INFO: 127.0.0.1:43062 - "GET /api/admin/inventory HTTP/1.1" 200 OK
2026-02-01 16:30:11 -06:00: INFO: 127.0.0.1:3048 - "GET /api/health HTTP/1.1" 200 OK
2026-02-01 16:33:27 -06:00: INFO: 127.0.0.1:14622 - "GET /api/health HTTP/1.1" 200 OK
2026-02-01 16:37:50 -06:00: INFO: 127.0.0.1:60700 - "GET /api/health HTTP/1.1" 200 OK
2026-02-01 16:39:10 -06:00: INFO: 127.0.0.1:8654 - "GET /api/health HTTP/1.1" 200 OK
2026-02-01 16:39:29 -06:00: INFO: 127.0.0.1:57386 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 16:39:29 -06:00: INFO: 127.0.0.1:57408 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 16:39:29 -06:00: INFO: 127.0.0.1:57408 - "GET /api/cart HTTP/1.1" 401 Unauthorized
2026-02-01 16:39:29 -06:00: INFO: 127.0.0.1:57402 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 16:39:29 -06:00: INFO: 127.0.0.1:57416 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 16:39:29 -06:00: INFO: 127.0.0.1:57408 - "GET /api/auth/me HTTP/1.1" 401 Unauthorized
2026-02-01 16:39:33 -06:00: INFO: 127.0.0.1:57408 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 16:39:33 -06:00: INFO: 127.0.0.1:15360 - "GET /uploads/products/da21b947-83d7-433c-a11d-3fb170721821.jpg HTTP/1.1" 200 OK
2026-02-01 16:41:52 -06:00: INFO: 127.0.0.1:53088 - "GET /api/health HTTP/1.1" 200 OK
2026-02-01 16:43:26 -06:00: INFO: 127.0.0.1:25110 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 16:43:29 -06:00: INFO: 127.0.0.1:25110 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 16:43:29 -06:00: INFO: 127.0.0.1:25122 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 16:43:32 -06:00: INFO: 127.0.0.1:25122 - "GET /api/products/1dd042e3-963d-430f-89c2-1eee235b345a HTTP/1.1" 200 OK
2026-02-01 16:56:22 -06:00: INFO: 127.0.0.1:2068 - "OPTIONS /api/auth/login HTTP/1.1" 200 OK
2026-02-01 16:56:22 -06:00: INFO: 127.0.0.1:2068 - "POST /api/auth/login HTTP/1.1" 401 Unauthorized
2026-02-01 16:56:30 -06:00: INFO: 127.0.0.1:2078 - "POST /api/auth/login HTTP/1.1" 401 Unauthorized
2026-02-01 16:57:19 -06:00: INFO: 127.0.0.1:38080 - "POST /api/auth/login HTTP/1.1" 401 Unauthorized
2026-02-01 16:57:35 -06:00: INFO: 127.0.0.1:38088 - "POST /api/auth/login HTTP/1.1" 401 Unauthorized
2026-02-01 16:58:01 -06:00: INFO: 127.0.0.1:30140 - "POST /api/auth/login HTTP/1.1" 401 Unauthorized
2026-02-01 17:06:31 -06:00: INFO: 127.0.0.1:48510 - "GET /api/health HTTP/1.1" 200 OK
2026-02-01 17:06:41 -06:00: INFO: 127.0.0.1:45960 - "POST /api/auth/login HTTP/1.1" 401 Unauthorized
2026-02-01 17:06:59 -06:00: INFO: 127.0.0.1:28268 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 17:07:03 -06:00: INFO: 127.0.0.1:59896 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 17:07:15 -06:00: INFO: 127.0.0.1:45226 - "OPTIONS /api/auth/login HTTP/1.1" 200 OK
2026-02-01 17:07:15 -06:00: INFO: 127.0.0.1:45238 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 17:07:15 -06:00: INFO: 127.0.0.1:45238 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 17:07:15 -06:00: INFO: 127.0.0.1:45248 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 17:07:15 -06:00: INFO: 127.0.0.1:45238 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:07:15 -06:00: INFO: 127.0.0.1:45248 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:07:24 -06:00: INFO: 127.0.0.1:45262 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:07:24 -06:00: INFO: 127.0.0.1:8480 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:07:24 -06:00: INFO: 127.0.0.1:8480 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:07:25 -06:00: INFO: 127.0.0.1:8496 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:07:26 -06:00: INFO: 127.0.0.1:8496 - "OPTIONS /api/admin/users?skip=0&limit=20&search=&role=&status= HTTP/1.1" 200 OK
2026-02-01 17:07:26 -06:00: INFO: 127.0.0.1:8496 - "GET /api/admin/users?skip=0&limit=20&search=&role=&status= HTTP/1.1" 200 OK
2026-02-01 17:07:56 -06:00: INFO: 127.0.0.1:58692 - "OPTIONS /api/admin/users HTTP/1.1" 200 OK
2026-02-01 17:07:57 -06:00: INFO: 127.0.0.1:58704 - "POST /api/admin/users HTTP/1.1" 200 OK
2026-02-01 17:07:57 -06:00: INFO: 127.0.0.1:58704 - "GET /api/admin/users?skip=0&limit=20&search=&role=&status= HTTP/1.1" 200 OK
2026-02-01 17:08:13 -06:00: INFO: 127.0.0.1:58706 - "POST /api/auth/login HTTP/1.1" 401 Unauthorized
2026-02-01 17:08:21 -06:00: INFO: 127.0.0.1:63300 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 17:08:21 -06:00: INFO: 127.0.0.1:63300 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 17:08:21 -06:00: INFO: 127.0.0.1:63308 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 17:08:21 -06:00: INFO: 127.0.0.1:63308 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:08:26 -06:00: INFO: 127.0.0.1:63330 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 17:08:27 -06:00: INFO: 127.0.0.1:63330 - "GET /api/products/1dd042e3-963d-430f-89c2-1eee235b345a HTTP/1.1" 200 OK
2026-02-01 17:08:30 -06:00: INFO: 127.0.0.1:63316 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:08:30 -06:00: INFO: 127.0.0.1:63330 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:08:32 -06:00: INFO: 127.0.0.1:63330 - "OPTIONS /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 17:08:32 -06:00: INFO: 127.0.0.1:63330 - "GET /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 17:08:34 -06:00: INFO: 127.0.0.1:63330 - "OPTIONS /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 17:08:34 -06:00: INFO: 127.0.0.1:63316 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 17:08:35 -06:00: INFO: 127.0.0.1:63022 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 17:11:18 -06:00: INFO: 127.0.0.1:10630 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:11:18 -06:00: INFO: 127.0.0.1:10632 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:11:18 -06:00: INFO: 127.0.0.1:10632 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:11:18 -06:00: INFO: 127.0.0.1:10632 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:16:28 -06:00: INFO: 127.0.0.1:51272 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 17:16:28 -06:00: INFO: 127.0.0.1:51280 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 17:17:08 -06:00: INFO: 127.0.0.1:51284 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:17:08 -06:00: INFO: 127.0.0.1:51284 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:17:08 -06:00: INFO: 127.0.0.1:22960 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:17:08 -06:00: INFO: 127.0.0.1:22980 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:19:07 -06:00: INFO: 127.0.0.1:7002 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:19:07 -06:00: INFO: 127.0.0.1:7006 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:19:07 -06:00: INFO: 127.0.0.1:7002 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:19:07 -06:00: INFO: 127.0.0.1:7006 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:26:28 -06:00: INFO: 127.0.0.1:13572 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:26:28 -06:00: INFO: 127.0.0.1:13582 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:26:29 -06:00: INFO: 127.0.0.1:13572 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:26:29 -06:00: INFO: 127.0.0.1:13572 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:32:45 -06:00: INFO: 127.0.0.1:15824 - "GET /api/health HTTP/1.1" 200 OK
2026-02-01 17:32:57 -06:00: INFO: 127.0.0.1:30510 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:32:58 -06:00: INFO: 127.0.0.1:30512 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:32:58 -06:00: INFO: 127.0.0.1:30510 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:32:58 -06:00: INFO: 127.0.0.1:30512 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:32:58 -06:00: INFO: 127.0.0.1:30512 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:32:58 -06:00: INFO: 127.0.0.1:30510 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:33:00 -06:00: INFO: 127.0.0.1:30510 - "OPTIONS /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 17:33:00 -06:00: INFO: 127.0.0.1:30510 - "GET /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 17:33:14 -06:00: INFO: 127.0.0.1:30516 - "OPTIONS /api/media/upload HTTP/1.1" 200 OK
2026-02-01 17:33:14 -06:00: INFO: 127.0.0.1:30516 - "POST /api/media/upload HTTP/1.1" 500 Internal Server Error
2026-02-01 17:33:38 -06:00: INFO: 127.0.0.1:30518 - "POST /api/media/upload HTTP/1.1" 500 Internal Server Error
2026-02-01 17:33:42 -06:00: INFO: 127.0.0.1:30518 - "POST /api/media/upload HTTP/1.1" 500 Internal Server Error
2026-02-01 17:34:45 -06:00: INFO: 127.0.0.1:13614 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:34:45 -06:00: INFO: 127.0.0.1:13618 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:34:45 -06:00: INFO: 127.0.0.1:13618 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:34:45 -06:00: INFO: 127.0.0.1:13614 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:34:45 -06:00: INFO: 127.0.0.1:13618 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:34:45 -06:00: INFO: 127.0.0.1:13614 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:36:00 -06:00: INFO: 127.0.0.1:35118 - "GET /api/health HTTP/1.1" 200 OK
2026-02-01 17:36:09 -06:00: INFO: 127.0.0.1:38080 - "GET /api/health HTTP/1.1" 200 OK
2026-02-01 17:36:20 -06:00: INFO: 127.0.0.1:56340 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:36:20 -06:00: INFO: 127.0.0.1:56356 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:36:20 -06:00: INFO: 127.0.0.1:56340 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:36:20 -06:00: INFO: 127.0.0.1:56356 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:36:20 -06:00: INFO: 127.0.0.1:56340 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:36:20 -06:00: INFO: 127.0.0.1:56356 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:36:21 -06:00: INFO: 127.0.0.1:56356 - "GET /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 17:36:25 -06:00: INFO: 127.0.0.1:56356 - "POST /api/media/upload HTTP/1.1" 200 OK
2026-02-01 17:36:25 -06:00: INFO: 127.0.0.1:56356 - "GET /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 17:36:25 -06:00: INFO: 127.0.0.1:53026 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 200 OK
2026-02-01 17:36:42 -06:00: INFO: 127.0.0.1:56366 - "OPTIONS /api/media/50704a79-caf9-437e-8047-a9ab3ad944aa HTTP/1.1" 200 OK
2026-02-01 17:36:42 -06:00: INFO: 127.0.0.1:56366 - "PUT /api/media/50704a79-caf9-437e-8047-a9ab3ad944aa HTTP/1.1" 200 OK
2026-02-01 17:36:42 -06:00: INFO: 127.0.0.1:56366 - "GET /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 17:36:50 -06:00: INFO: 127.0.0.1:45298 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:36:50 -06:00: INFO: 127.0.0.1:45314 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:36:50 -06:00: INFO: 127.0.0.1:45298 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:36:50 -06:00: INFO: 127.0.0.1:45314 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:36:50 -06:00: INFO: 127.0.0.1:45314 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:36:50 -06:00: INFO: 127.0.0.1:45298 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:36:50 -06:00: INFO: 127.0.0.1:45298 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:36:50 -06:00: INFO: 127.0.0.1:45316 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:36:52 -06:00: INFO: 127.0.0.1:45316 - "GET /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 17:36:52 -06:00: INFO: 127.0.0.1:53038 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 304 Not Modified
2026-02-01 17:37:09 -06:00: INFO: 127.0.0.1:45318 - "PUT /api/media/50704a79-caf9-437e-8047-a9ab3ad944aa HTTP/1.1" 200 OK
2026-02-01 17:37:09 -06:00: INFO: 127.0.0.1:45318 - "GET /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 17:37:53 -06:00: INFO: 127.0.0.1:12800 - "OPTIONS /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 17:37:53 -06:00: INFO: 127.0.0.1:12808 - "OPTIONS /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 17:37:53 -06:00: INFO: 127.0.0.1:12824 - "OPTIONS /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 17:37:53 -06:00: INFO: 127.0.0.1:12800 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 17:37:53 -06:00: INFO: 127.0.0.1:12824 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 17:37:53 -06:00: INFO: 127.0.0.1:12808 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 17:39:15 -06:00: INFO: 127.0.0.1:45602 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:39:15 -06:00: INFO: 127.0.0.1:45610 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:39:15 -06:00: INFO: 127.0.0.1:45602 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:39:15 -06:00: INFO: 127.0.0.1:45602 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:39:15 -06:00: INFO: 127.0.0.1:45602 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:39:15 -06:00: INFO: 127.0.0.1:45614 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:39:17 -06:00: INFO: 127.0.0.1:45614 - "GET /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 17:39:18 -06:00: INFO: 127.0.0.1:45648 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 304 Not Modified
2026-02-01 17:44:50 -06:00: INFO: 127.0.0.1:55690 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:44:50 -06:00: INFO: 127.0.0.1:55698 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:44:50 -06:00: INFO: 127.0.0.1:55698 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:44:50 -06:00: INFO: 127.0.0.1:55698 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:44:50 -06:00: INFO: 127.0.0.1:55698 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:44:50 -06:00: INFO: 127.0.0.1:55700 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:44:52 -06:00: INFO: 127.0.0.1:55700 - "OPTIONS /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 17:44:52 -06:00: INFO: 127.0.0.1:55698 - "GET /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 17:44:52 -06:00: INFO: 127.0.0.1:23150 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 304 Not Modified
2026-02-01 17:45:20 -06:00: INFO: 127.0.0.1:55704 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 17:45:20 -06:00: INFO: 127.0.0.1:55714 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 17:45:20 -06:00: INFO: 127.0.0.1:55722 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 17:45:35 -06:00: INFO: 127.0.0.1:38428 - "GET /api/media HTTP/1.1" 403 Forbidden
2026-02-01 17:45:42 -06:00: INFO: 127.0.0.1:50368 - "GET /api/media HTTP/1.1" 403 Forbidden
2026-02-01 17:46:05 -06:00: INFO: 127.0.0.1:36942 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:46:05 -06:00: INFO: 127.0.0.1:36926 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:46:06 -06:00: INFO: 127.0.0.1:36942 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 17:46:06 -06:00: INFO: 127.0.0.1:36926 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 17:46:06 -06:00: INFO: 127.0.0.1:36952 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 17:46:53 -06:00: INFO: 127.0.0.1:15764 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:46:53 -06:00: INFO: 127.0.0.1:15768 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:46:53 -06:00: INFO: 127.0.0.1:15768 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:46:53 -06:00: INFO: 127.0.0.1:15782 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 200 OK
2026-02-01 17:46:53 -06:00: INFO: 127.0.0.1:15764 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:49:40 -06:00: INFO: 127.0.0.1:17196 - "HEAD /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 200 OK
2026-02-01 17:51:07 -06:00: INFO: ::1:0 - "HEAD /uploads/media/test.txt HTTP/1.1" 404 Not Found
2026-02-01 17:56:52 -06:00: INFO: ::1:0 - "HEAD /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 200 OK
2026-02-01 17:57:24 -06:00: INFO: 127.0.0.1:17680 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 304 Not Modified
2026-02-01 17:57:26 -06:00: INFO: 127.0.0.1:17694 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:57:26 -06:00: INFO: 127.0.0.1:17680 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:57:26 -06:00: INFO: None:0 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 200 OK
2026-02-01 17:57:26 -06:00: INFO: 127.0.0.1:17680 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:57:26 -06:00: INFO: 127.0.0.1:17694 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 18:05:46 -06:00: INFO: 127.0.0.1:59026 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 18:05:46 -06:00: INFO: 127.0.0.1:59022 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 18:08:10 -06:00: INFO: 127.0.0.1:45434 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:08:10 -06:00: INFO: 127.0.0.1:45438 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 18:08:11 -06:00: INFO: 127.0.0.1:45434 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:08:11 -06:00: INFO: 127.0.0.1:45438 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 18:10:21 -06:00: INFO: 127.0.0.1:57524 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 18:10:21 -06:00: INFO: 127.0.0.1:57540 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 18:10:21 -06:00: INFO: 127.0.0.1:57540 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 18:10:21 -06:00: INFO: 127.0.0.1:57540 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 18:10:25 -06:00: INFO: 127.0.0.1:57540 - "OPTIONS /api/admin/inventory HTTP/1.1" 200 OK
2026-02-01 18:10:25 -06:00: INFO: 127.0.0.1:57540 - "GET /api/admin/inventory HTTP/1.1" 200 OK
2026-02-01 18:10:27 -06:00: INFO: 127.0.0.1:57540 - "OPTIONS /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 18:10:27 -06:00: INFO: 127.0.0.1:57540 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 18:10:28 -06:00: INFO: 127.0.0.1:57540 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 18:10:30 -06:00: INFO: 127.0.0.1:57540 - "OPTIONS /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 18:10:30 -06:00: INFO: 127.0.0.1:57540 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 18:10:31 -06:00: INFO: 127.0.0.1:2606 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 18:10:31 -06:00: INFO: 127.0.0.1:57540 - "OPTIONS /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 18:10:31 -06:00: INFO: 127.0.0.1:2580 - "GET /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 18:10:33 -06:00: INFO: 127.0.0.1:2584 - "OPTIONS /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 18:10:33 -06:00: INFO: 127.0.0.1:2580 - "OPTIONS /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 18:10:33 -06:00: INFO: 127.0.0.1:2592 - "OPTIONS /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 18:10:33 -06:00: INFO: 127.0.0.1:2580 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 18:10:33 -06:00: INFO: 127.0.0.1:2584 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 18:10:33 -06:00: INFO: 127.0.0.1:2592 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 18:16:07 -06:00: INFO: 127.0.0.1:37524 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:16:07 -06:00: INFO: 127.0.0.1:37528 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 18:16:07 -06:00: INFO: 127.0.0.1:37524 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:16:07 -06:00: INFO: 127.0.0.1:37528 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 18:16:07 -06:00: INFO: 127.0.0.1:37524 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 18:16:07 -06:00: INFO: 127.0.0.1:37528 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 18:16:08 -06:00: INFO: 127.0.0.1:37524 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 18:16:08 -06:00: INFO: 127.0.0.1:37528 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 18:16:08 -06:00: INFO: 127.0.0.1:37540 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 18:16:12 -06:00: INFO: 127.0.0.1:37540 - "OPTIONS /api/admin/about/team/780a7413-d421-4a7a-8c51-f45368215490 HTTP/1.1" 200 OK
2026-02-01 18:16:12 -06:00: INFO: 127.0.0.1:37540 - "DELETE /api/admin/about/team/780a7413-d421-4a7a-8c51-f45368215490 HTTP/1.1" 500 Internal Server Error
2026-02-01 18:37:23 -06:00: INFO: 127.0.0.1:25648 - "OPTIONS /api/admin/about/team/test HTTP/1.1" 200 OK
2026-02-01 18:37:34 -06:00: INFO: 127.0.0.1:10448 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 18:37:34 -06:00: INFO: 127.0.0.1:10454 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:37:34 -06:00: INFO: 127.0.0.1:10448 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 18:37:34 -06:00: INFO: 127.0.0.1:10454 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:37:34 -06:00: INFO: 127.0.0.1:10448 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 18:37:34 -06:00: INFO: 127.0.0.1:10454 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 18:37:34 -06:00: INFO: 127.0.0.1:10454 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 18:37:34 -06:00: INFO: 127.0.0.1:10466 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 18:37:36 -06:00: INFO: 127.0.0.1:10454 - "OPTIONS /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 18:37:36 -06:00: INFO: 127.0.0.1:10466 - "OPTIONS /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 18:37:36 -06:00: INFO: 127.0.0.1:10478 - "OPTIONS /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 18:37:36 -06:00: INFO: 127.0.0.1:10454 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 18:37:36 -06:00: INFO: 127.0.0.1:10466 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 18:37:36 -06:00: INFO: 127.0.0.1:10484 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 18:37:39 -06:00: INFO: 127.0.0.1:10484 - "OPTIONS /api/admin/about/team/780a7413-d421-4a7a-8c51-f45368215490 HTTP/1.1" 200 OK
2026-02-01 18:37:39 -06:00: INFO: 127.0.0.1:10484 - "DELETE /api/admin/about/team/780a7413-d421-4a7a-8c51-f45368215490 HTTP/1.1" 500 Internal Server Error
2026-02-01 18:40:11 -06:00: INFO: 127.0.0.1:47374 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 18:40:11 -06:00: INFO: 127.0.0.1:47388 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:40:11 -06:00: INFO: 127.0.0.1:47388 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 18:40:11 -06:00: INFO: 127.0.0.1:47388 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:40:11 -06:00: INFO: 127.0.0.1:47392 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 18:40:11 -06:00: INFO: 127.0.0.1:47388 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 18:40:12 -06:00: INFO: 127.0.0.1:47392 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 18:40:12 -06:00: INFO: 127.0.0.1:47388 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 18:40:13 -06:00: INFO: 127.0.0.1:47400 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 18:40:16 -06:00: INFO: 127.0.0.1:47400 - "DELETE /api/admin/about/team/780a7413-d421-4a7a-8c51-f45368215490 HTTP/1.1" 200 OK
2026-02-01 18:40:16 -06:00: INFO: 127.0.0.1:47400 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 18:40:16 -06:00: INFO: 127.0.0.1:47388 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 18:40:16 -06:00: INFO: 127.0.0.1:47392 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 18:40:19 -06:00: INFO: 127.0.0.1:47392 - "OPTIONS /api/admin/about/team/27ea484b-d3ce-4f32-9fe0-c17e4f9db685 HTTP/1.1" 200 OK
2026-02-01 18:40:19 -06:00: INFO: 127.0.0.1:47392 - "DELETE /api/admin/about/team/27ea484b-d3ce-4f32-9fe0-c17e4f9db685 HTTP/1.1" 200 OK
2026-02-01 18:40:19 -06:00: INFO: 127.0.0.1:47388 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 18:40:19 -06:00: INFO: 127.0.0.1:47392 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 18:40:19 -06:00: INFO: 127.0.0.1:47400 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 18:40:21 -06:00: INFO: 127.0.0.1:47392 - "OPTIONS /api/admin/about/team/ac3f6f6d-c190-4108-b4be-567a091f3f6b HTTP/1.1" 200 OK
2026-02-01 18:40:21 -06:00: INFO: 127.0.0.1:47392 - "DELETE /api/admin/about/team/ac3f6f6d-c190-4108-b4be-567a091f3f6b HTTP/1.1" 200 OK
2026-02-01 18:40:21 -06:00: INFO: 127.0.0.1:47392 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 18:40:21 -06:00: INFO: 127.0.0.1:47388 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 18:40:21 -06:00: INFO: 127.0.0.1:47400 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 18:40:23 -06:00: INFO: 127.0.0.1:47400 - "OPTIONS /api/admin/about/team/c01a0641-8bab-4266-a4c4-f3355c8968aa HTTP/1.1" 200 OK
2026-02-01 18:40:23 -06:00: INFO: 127.0.0.1:47400 - "DELETE /api/admin/about/team/c01a0641-8bab-4266-a4c4-f3355c8968aa HTTP/1.1" 200 OK
2026-02-01 18:40:23 -06:00: INFO: 127.0.0.1:47388 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 18:40:23 -06:00: INFO: 127.0.0.1:47392 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 18:40:23 -06:00: INFO: 127.0.0.1:47400 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 18:40:30 -06:00: INFO: 127.0.0.1:47412 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 18:40:30 -06:00: INFO: 127.0.0.1:47412 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:40:30 -06:00: INFO: None:0 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 200 OK
2026-02-01 18:40:31 -06:00: INFO: 127.0.0.1:65534 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:40:31 -06:00: INFO: 127.0.0.1:1026 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 18:49:18 -06:00: INFO: 127.0.0.1:62498 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 18:49:18 -06:00: INFO: 127.0.0.1:62492 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 18:49:18 -06:00: INFO: 127.0.0.1:62520 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:49:18 -06:00: INFO: 127.0.0.1:62500 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 18:49:18 -06:00: INFO: 127.0.0.1:62492 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 18:49:18 -06:00: INFO: 127.0.0.1:62510 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 18:49:18 -06:00: INFO: 127.0.0.1:62520 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:49:27 -06:00: INFO: 127.0.0.1:62524 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 18:49:27 -06:00: INFO: 127.0.0.1:62524 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 18:49:27 -06:00: INFO: 127.0.0.1:24374 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 18:49:27 -06:00: INFO: 127.0.0.1:24354 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 18:49:35 -06:00: INFO: 127.0.0.1:24366 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 18:49:35 -06:00: INFO: 127.0.0.1:24366 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 18:50:01 -06:00: INFO: 127.0.0.1:47670 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 18:50:03 -06:00: INFO: 127.0.0.1:47670 - "GET /api/products/d5b1c882-f802-42d9-877b-5fab63156f54 HTTP/1.1" 200 OK
2026-02-01 18:50:17 -06:00: INFO: 127.0.0.1:6902 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 18:50:19 -06:00: INFO: 127.0.0.1:6912 - "GET /api/services/9c70d282-8b80-45d0-ba38-e93bd8f0479c HTTP/1.1" 200 OK
2026-02-01 18:50:33 -06:00: INFO: 127.0.0.1:6908 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 18:57:06 -06:00: INFO: 127.0.0.1:27250 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 18:57:06 -06:00: INFO: 127.0.0.1:27260 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:57:06 -06:00: INFO: 127.0.0.1:27242 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 18:57:06 -06:00: INFO: 127.0.0.1:27260 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:57:06 -06:00: INFO: 127.0.0.1:27250 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 18:57:08 -06:00: INFO: 127.0.0.1:27242 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:57:08 -06:00: INFO: 127.0.0.1:27260 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 18:57:08 -06:00: INFO: 127.0.0.1:27250 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 18:57:08 -06:00: INFO: 127.0.0.1:27260 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:57:08 -06:00: INFO: 127.0.0.1:27242 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 18:57:09 -06:00: INFO: 127.0.0.1:27270 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 18:57:49 -06:00: INFO: 127.0.0.1:27266 - "GET /api/services?category=repair HTTP/1.1" 200 OK
2026-02-01 18:57:49 -06:00: INFO: 127.0.0.1:27284 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 18:58:45 -06:00: INFO: 127.0.0.1:55632 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 18:58:45 -06:00: INFO: 127.0.0.1:55654 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 18:58:45 -06:00: INFO: 127.0.0.1:55648 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:03:30 -06:00: INFO: 127.0.0.1:29532 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 19:03:30 -06:00: INFO: 127.0.0.1:29572 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:03:30 -06:00: INFO: 127.0.0.1:29524 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 19:03:30 -06:00: INFO: 127.0.0.1:29544 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:03:30 -06:00: INFO: 127.0.0.1:29558 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 19:03:30 -06:00: INFO: 127.0.0.1:29558 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:03:30 -06:00: INFO: 127.0.0.1:29572 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 19:03:47 -06:00: INFO: 127.0.0.1:29588 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 19:03:50 -06:00: INFO: 127.0.0.1:29604 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 19:03:50 -06:00: INFO: 127.0.0.1:29604 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:03:50 -06:00: INFO: 127.0.0.1:29588 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 19:03:50 -06:00: INFO: 127.0.0.1:29588 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 19:03:50 -06:00: INFO: 127.0.0.1:29588 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:03:52 -06:00: INFO: 127.0.0.1:35864 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 19:04:51 -06:00: INFO: 127.0.0.1:24658 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 19:04:52 -06:00: INFO: 127.0.0.1:24658 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 19:04:52 -06:00: INFO: 127.0.0.1:24662 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 19:04:52 -06:00: INFO: 127.0.0.1:24658 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:06:00 -06:00: INFO: 127.0.0.1:19770 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:06:00 -06:00: INFO: 127.0.0.1:19778 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:06:00 -06:00: INFO: 127.0.0.1:19778 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:06:00 -06:00: INFO: 127.0.0.1:19770 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:06:01 -06:00: INFO: 127.0.0.1:19770 - "OPTIONS /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:06:01 -06:00: INFO: 127.0.0.1:19778 - "OPTIONS /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:06:01 -06:00: INFO: 127.0.0.1:19782 - "OPTIONS /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:06:01 -06:00: INFO: 127.0.0.1:19782 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:06:01 -06:00: INFO: 127.0.0.1:19778 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:06:01 -06:00: INFO: 127.0.0.1:19770 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:08:51 -06:00: INFO: 127.0.0.1:1378 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 19:08:52 -06:00: INFO: 127.0.0.1:1392 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 19:08:52 -06:00: INFO: 127.0.0.1:1396 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:08:54 -06:00: INFO: 127.0.0.1:1398 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 19:08:54 -06:00: INFO: 127.0.0.1:1378 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:08:54 -06:00: INFO: 127.0.0.1:1396 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 19:08:54 -06:00: INFO: 127.0.0.1:1392 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 19:08:54 -06:00: INFO: 127.0.0.1:1398 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:08:54 -06:00: INFO: None:0 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 200 OK
2026-02-01 19:08:54 -06:00: INFO: 127.0.0.1:1392 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:08:54 -06:00: INFO: 127.0.0.1:1396 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 19:09:22 -06:00: INFO: 127.0.0.1:1426 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 19:09:22 -06:00: INFO: 127.0.0.1:1418 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 19:09:22 -06:00: INFO: 127.0.0.1:1426 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:09:42 -06:00: INFO: 127.0.0.1:49438 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:09:42 -06:00: INFO: 127.0.0.1:49438 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:09:45 -06:00: INFO: 127.0.0.1:31618 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:09:45 -06:00: INFO: 127.0.0.1:49438 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:09:45 -06:00: INFO: 127.0.0.1:31618 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:11:32 -06:00: INFO: 127.0.0.1:36300 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:11:39 -06:00: INFO: 127.0.0.1:36310 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 19:11:45 -06:00: INFO: 127.0.0.1:25574 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 19:15:14 -06:00: INFO: 127.0.0.1:63146 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:15:39 -06:00: INFO: 127.0.0.1:49584 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 19:15:39 -06:00: INFO: 127.0.0.1:49596 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:15:40 -06:00: INFO: 127.0.0.1:49596 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 19:15:40 -06:00: INFO: 127.0.0.1:49596 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:15:40 -06:00: INFO: 127.0.0.1:49596 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:15:40 -06:00: INFO: 127.0.0.1:49610 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:15:42 -06:00: INFO: 127.0.0.1:49610 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:15:42 -06:00: INFO: 127.0.0.1:49596 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:15:42 -06:00: INFO: 127.0.0.1:49614 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:16:00 -06:00: INFO: 127.0.0.1:49628 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 19:16:00 -06:00: INFO: 127.0.0.1:49638 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 19:16:00 -06:00: INFO: 127.0.0.1:49628 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:16:02 -06:00: INFO: 127.0.0.1:49628 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 19:16:02 -06:00: INFO: 127.0.0.1:49638 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 19:16:02 -06:00: INFO: 127.0.0.1:45578 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:16:02 -06:00: INFO: 127.0.0.1:49628 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 19:16:02 -06:00: INFO: 127.0.0.1:49638 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:16:02 -06:00: INFO: None:0 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 200 OK
2026-02-01 19:16:02 -06:00: INFO: 127.0.0.1:45578 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 19:16:02 -06:00: INFO: 127.0.0.1:45578 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:16:24 -06:00: INFO: 127.0.0.1:14394 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:16:24 -06:00: INFO: 127.0.0.1:14388 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:16:24 -06:00: INFO: 127.0.0.1:14394 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:16:24 -06:00: INFO: 127.0.0.1:14388 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:16:27 -06:00: INFO: 127.0.0.1:14394 - "OPTIONS /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:16:27 -06:00: INFO: 127.0.0.1:14388 - "OPTIONS /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:16:27 -06:00: INFO: 127.0.0.1:14408 - "OPTIONS /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:16:27 -06:00: INFO: 127.0.0.1:14408 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:16:27 -06:00: INFO: 127.0.0.1:14394 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:16:27 -06:00: INFO: 127.0.0.1:14388 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:18:24 -06:00: INFO: 127.0.0.1:16868 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 19:18:24 -06:00: INFO: 127.0.0.1:16870 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:21:44 -06:00: INFO: 127.0.0.1:65258 - "POST /api/token HTTP/1.1" 404 Not Found
2026-02-01 19:21:44 -06:00: INFO: 127.0.0.1:65274 - "GET /api/admin/about/content HTTP/1.1" 401 Unauthorized
2026-02-01 19:21:55 -06:00: INFO: 127.0.0.1:51116 - "POST /api/token HTTP/1.1" 404 Not Found
2026-02-01 19:21:55 -06:00: INFO: 127.0.0.1:51118 - "GET /api/admin/about/content HTTP/1.1" 401 Unauthorized
2026-02-01 19:22:06 -06:00: INFO: 127.0.0.1:24376 - "POST /api/token HTTP/1.1" 404 Not Found
2026-02-01 19:22:06 -06:00: INFO: 127.0.0.1:24384 - "GET /api/admin/about/content HTTP/1.1" 401 Unauthorized
2026-02-01 19:22:13 -06:00: INFO: 127.0.0.1:19636 - "POST /api/token HTTP/1.1" 404 Not Found
2026-02-01 19:22:13 -06:00: INFO: 127.0.0.1:19652 - "GET /api/admin/about/content HTTP/1.1" 401 Unauthorized
2026-02-01 19:22:41 -06:00: INFO: 127.0.0.1:64748 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 19:22:41 -06:00: INFO: 127.0.0.1:64756 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:22:58 -06:00: INFO: 127.0.0.1:16298 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:22:58 -06:00: INFO: 127.0.0.1:16308 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 19:22:58 -06:00: INFO: 127.0.0.1:16308 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:22:58 -06:00: INFO: 127.0.0.1:16308 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 19:22:58 -06:00: INFO: 127.0.0.1:16308 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:22:58 -06:00: INFO: 127.0.0.1:16314 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:22:59 -06:00: INFO: 127.0.0.1:16308 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:22:59 -06:00: INFO: 127.0.0.1:16314 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:22:59 -06:00: INFO: 127.0.0.1:16320 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:23:23 -06:00: INFO: 127.0.0.1:16328 - "OPTIONS /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 19:23:24 -06:00: INFO: 127.0.0.1:16328 - "GET /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 19:23:24 -06:00: INFO: 127.0.0.1:28126 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 304 Not Modified
2026-02-01 19:23:26 -06:00: INFO: 127.0.0.1:16328 - "OPTIONS /api/admin/reports/sales?period=monthly HTTP/1.1" 200 OK
2026-02-01 19:23:26 -06:00: INFO: 127.0.0.1:16328 - "GET /api/admin/reports/sales?period=monthly HTTP/1.1" 200 OK
2026-02-01 19:23:31 -06:00: INFO: 127.0.0.1:16342 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 19:23:31 -06:00: INFO: 127.0.0.1:16328 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 19:24:27 -06:00: INFO: 127.0.0.1:46252 - "OPTIONS /api/orders HTTP/1.1" 200 OK
2026-02-01 19:24:27 -06:00: INFO: 127.0.0.1:46252 - "GET /api/orders HTTP/1.1" 200 OK
2026-02-01 19:24:30 -06:00: INFO: 127.0.0.1:46252 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 19:24:32 -06:00: INFO: 127.0.0.1:58148 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 19:24:40 -06:00: INFO: 127.0.0.1:46266 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 19:24:40 -06:00: INFO: 127.0.0.1:46266 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 19:24:40 -06:00: INFO: 127.0.0.1:46266 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:28:42 -06:00: INFO: 127.0.0.1:62306 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:28:42 -06:00: INFO: 127.0.0.1:62316 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:28:42 -06:00: INFO: 127.0.0.1:62306 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:28:42 -06:00: INFO: 127.0.0.1:62316 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:28:44 -06:00: INFO: 127.0.0.1:62316 - "OPTIONS /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:28:44 -06:00: INFO: 127.0.0.1:62306 - "OPTIONS /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:28:44 -06:00: INFO: 127.0.0.1:62318 - "OPTIONS /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:28:44 -06:00: INFO: 127.0.0.1:62316 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:28:44 -06:00: INFO: 127.0.0.1:62318 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:28:44 -06:00: INFO: 127.0.0.1:62306 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:28:59 -06:00: INFO: 127.0.0.1:7890 - "OPTIONS /api/admin/about/content/71f29e56-a68b-4114-a931-e8fd08a787df HTTP/1.1" 200 OK
2026-02-01 19:28:59 -06:00: INFO: 127.0.0.1:7890 - "PUT /api/admin/about/content/71f29e56-a68b-4114-a931-e8fd08a787df HTTP/1.1" 500 Internal Server Error
2026-02-01 19:35:40 -06:00: INFO: 127.0.0.1:62240 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 19:35:40 -06:00: INFO: 127.0.0.1:62246 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:35:40 -06:00: INFO: 127.0.0.1:62260 - "PUT /api/admin/about/content/71f29e56-a68b-4114-a931-e8fd08a787df HTTP/1.1" 500 Internal Server Error
2026-02-01 19:35:54 -06:00: INFO: 127.0.0.1:42960 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 19:35:54 -06:00: INFO: 127.0.0.1:42970 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:35:54 -06:00: INFO: 127.0.0.1:42978 - "PUT /api/admin/about/content/71f29e56-a68b-4114-a931-e8fd08a787df HTTP/1.1" 500 Internal Server Error
2026-02-01 19:36:13 -06:00: INFO: 127.0.0.1:19682 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 19:36:13 -06:00: INFO: 127.0.0.1:19684 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:36:13 -06:00: INFO: 127.0.0.1:19692 - "PUT /api/admin/about/content/71f29e56-a68b-4114-a931-e8fd08a787df HTTP/1.1" 500 Internal Server Error
2026-02-01 19:39:46 -06:00: INFO: 127.0.0.1:28416 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 19:39:46 -06:00: INFO: 127.0.0.1:28432 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:39:46 -06:00: INFO: 127.0.0.1:28440 - "PUT /api/admin/about/content/71f29e56-a68b-4114-a931-e8fd08a787df HTTP/1.1" 200 OK
2026-02-01 19:40:10 -06:00: INFO: 127.0.0.1:60304 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 19:40:10 -06:00: INFO: 127.0.0.1:60320 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:40:10 -06:00: INFO: 127.0.0.1:60326 - "PUT /api/admin/about/content/71f29e56-a68b-4114-a931-e8fd08a787df HTTP/1.1" 200 OK
2026-02-01 19:40:44 -06:00: INFO: 127.0.0.1:34302 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:40:54 -06:00: INFO: 127.0.0.1:63638 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:41:26 -06:00: INFO: 127.0.0.1:11612 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:50:36 -06:00: INFO: 127.0.0.1:21932 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:50:36 -06:00: INFO: 127.0.0.1:21938 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 19:50:36 -06:00: INFO: 127.0.0.1:21938 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:50:36 -06:00: INFO: 127.0.0.1:21938 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:50:36 -06:00: INFO: 127.0.0.1:21938 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:50:36 -06:00: INFO: 127.0.0.1:21932 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 19:50:36 -06:00: INFO: 127.0.0.1:21932 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:50:36 -06:00: INFO: 127.0.0.1:21932 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:50:37 -06:00: INFO: 127.0.0.1:21952 - "OPTIONS /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:50:37 -06:00: INFO: 127.0.0.1:21932 - "OPTIONS /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:50:37 -06:00: INFO: 127.0.0.1:21958 - "OPTIONS /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:50:37 -06:00: INFO: 127.0.0.1:21966 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:50:37 -06:00: INFO: 127.0.0.1:21932 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:50:37 -06:00: INFO: 127.0.0.1:21958 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:50:47 -06:00: INFO: 127.0.0.1:21976 - "OPTIONS /api/admin/about/content/71f29e56-a68b-4114-a931-e8fd08a787df HTTP/1.1" 200 OK
2026-02-01 19:50:47 -06:00: INFO: 127.0.0.1:21976 - "PUT /api/admin/about/content/71f29e56-a68b-4114-a931-e8fd08a787df HTTP/1.1" 200 OK
2026-02-01 19:50:47 -06:00: INFO: 127.0.0.1:21976 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:50:47 -06:00: INFO: 127.0.0.1:21976 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:50:47 -06:00: INFO: 127.0.0.1:21976 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:50:49 -06:00: INFO: 127.0.0.1:21976 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 19:50:49 -06:00: INFO: 127.0.0.1:1254 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 19:50:49 -06:00: INFO: 127.0.0.1:1246 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:51:41 -06:00: INFO: 127.0.0.1:26774 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 19:51:44 -06:00: INFO: 127.0.0.1:11628 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 19:55:04 -06:00: INFO: 127.0.0.1:13980 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 19:55:04 -06:00: INFO: 127.0.0.1:13994 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:55:04 -06:00: INFO: 127.0.0.1:13994 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:55:04 -06:00: INFO: 127.0.0.1:13980 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 19:55:04 -06:00: INFO: 127.0.0.1:13998 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 19:55:07 -06:00: INFO: 127.0.0.1:13994 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 19:55:08 -06:00: INFO: 127.0.0.1:13994 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 19:55:11 -06:00: INFO: 127.0.0.1:14004 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:55:11 -06:00: INFO: 127.0.0.1:13994 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:55:13 -06:00: INFO: 127.0.0.1:13994 - "OPTIONS /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 19:55:13 -06:00: INFO: 127.0.0.1:13994 - "GET /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 19:55:46 -06:00: INFO: 127.0.0.1:46564 - "GET /api/products/1dd042e3-963d-430f-89c2-1eee235b345a HTTP/1.1" 200 OK
2026-02-01 19:56:07 -06:00: INFO: 127.0.0.1:46566 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:56:07 -06:00: INFO: 127.0.0.1:46566 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:56:09 -06:00: INFO: 127.0.0.1:46566 - "OPTIONS /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 19:56:09 -06:00: INFO: 127.0.0.1:46566 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 19:56:09 -06:00: INFO: 127.0.0.1:9634 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 19:56:10 -06:00: INFO: 127.0.0.1:46566 - "OPTIONS /api/admin/orders HTTP/1.1" 200 OK
2026-02-01 19:56:10 -06:00: INFO: 127.0.0.1:46566 - "GET /api/admin/orders HTTP/1.1" 200 OK
2026-02-01 19:56:11 -06:00: INFO: 127.0.0.1:9650 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 19:56:11 -06:00: INFO: 127.0.0.1:46566 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 19:56:48 -06:00: INFO: 127.0.0.1:9626 - "OPTIONS /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 19:56:48 -06:00: INFO: 127.0.0.1:9626 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 19:57:53 -06:00: INFO: 127.0.0.1:9394 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 19:57:53 -06:00: INFO: 127.0.0.1:9398 - "POST /api/admin/products HTTP/1.1" 422 Unprocessable Entity
2026-02-01 19:58:14 -06:00: INFO: 127.0.0.1:49552 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 19:58:14 -06:00: INFO: 127.0.0.1:49554 - "POST /api/admin/products HTTP/1.1" 200 OK
2026-02-01 19:58:48 -06:00: INFO: 127.0.0.1:19776 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 19:58:48 -06:00: INFO: 127.0.0.1:19780 - "PUT /api/admin/products/05d789f1-09f4-42f6-9315-33683833c96d HTTP/1.1" 200 OK
2026-02-01 19:59:00 -06:00: INFO: 127.0.0.1:35170 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 20:01:04 -06:00: INFO: 127.0.0.1:22834 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 20:01:04 -06:00: INFO: 127.0.0.1:22850 - "DELETE /api/admin/products/05d789f1-09f4-42f6-9315-33683833c96d HTTP/1.1" 200 OK
2026-02-01 20:01:51 -06:00: INFO: 127.0.0.1:26872 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 20:01:51 -06:00: INFO: 127.0.0.1:26876 - "GET /api/admin/services HTTP/1.1" 200 OK
2026-02-01 20:02:28 -06:00: INFO: 127.0.0.1:11592 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 20:02:28 -06:00: INFO: 127.0.0.1:11608 - "POST /api/admin/services HTTP/1.1" 200 OK
2026-02-01 20:03:18 -06:00: INFO: 127.0.0.1:40692 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 20:03:18 -06:00: INFO: 127.0.0.1:40698 - "PUT /api/admin/services/59459c6d-ff72-49a0-bba4-a4966ffa84e0 HTTP/1.1" 200 OK
2026-02-01 20:03:33 -06:00: INFO: 127.0.0.1:43930 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 20:03:46 -06:00: INFO: 127.0.0.1:58864 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 20:03:46 -06:00: INFO: 127.0.0.1:58874 - "DELETE /api/admin/services/59459c6d-ff72-49a0-bba4-a4966ffa84e0 HTTP/1.1" 200 OK
2026-02-01 20:07:47 -06:00: INFO: 127.0.0.1:30124 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 20:07:47 -06:00: INFO: 127.0.0.1:30130 - "GET /api/admin/inventory HTTP/1.1" 200 OK
2026-02-01 20:08:04 -06:00: INFO: 127.0.0.1:22338 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 20:08:04 -06:00: INFO: 127.0.0.1:22350 - "GET /api/admin/inventory HTTP/1.1" 200 OK
2026-02-01 20:08:45 -06:00: INFO: 127.0.0.1:22152 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 20:08:45 -06:00: INFO: 127.0.0.1:22160 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 20:09:01 -06:00: INFO: 127.0.0.1:13828 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 20:09:01 -06:00: INFO: 127.0.0.1:13844 - "POST /api/services/book HTTP/1.1" 200 OK
2026-02-01 20:09:12 -06:00: INFO: 127.0.0.1:27660 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 20:09:12 -06:00: INFO: 127.0.0.1:27664 - "POST /api/services/book HTTP/1.1" 200 OK
2026-02-01 20:09:41 -06:00: INFO: 127.0.0.1:24388 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 20:09:41 -06:00: INFO: 127.0.0.1:20202 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 20:21:04 -06:00: INFO: 127.0.0.1:33546 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 20:21:04 -06:00: INFO: 127.0.0.1:33558 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 20:21:04 -06:00: INFO: 127.0.0.1:33558 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 20:21:04 -06:00: INFO: 127.0.0.1:33558 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 20:21:04 -06:00: INFO: 127.0.0.1:33558 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 20:21:04 -06:00: INFO: 127.0.0.1:33558 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 20:21:04 -06:00: INFO: 127.0.0.1:33572 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 20:21:04 -06:00: INFO: 127.0.0.1:33572 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 20:21:06 -06:00: INFO: 127.0.0.1:33572 - "OPTIONS /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 20:21:06 -06:00: INFO: 127.0.0.1:33572 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 20:21:17 -06:00: INFO: 127.0.0.1:33588 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 20:21:18 -06:00: INFO: 127.0.0.1:18068 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 20:21:35 -06:00: INFO: 127.0.0.1:33598 - "GET /api/services/9c70d282-8b80-45d0-ba38-e93bd8f0479c HTTP/1.1" 200 OK
2026-02-01 20:22:41 -06:00: INFO: 127.0.0.1:33632 - "OPTIONS /api/services/book HTTP/1.1" 200 OK
2026-02-01 20:22:41 -06:00: INFO: 127.0.0.1:33632 - "POST /api/services/book HTTP/1.1" 200 OK
2026-02-01 20:22:52 -06:00: INFO: 127.0.0.1:33642 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 20:22:52 -06:00: INFO: 127.0.0.1:33642 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 20:22:54 -06:00: INFO: 127.0.0.1:33642 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 20:23:03 -06:00: INFO: 127.0.0.1:42694 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 20:23:06 -06:00: INFO: 127.0.0.1:42694 - "OPTIONS /api/admin/users?skip=0&limit=20&search=&role=&status= HTTP/1.1" 200 OK
2026-02-01 20:23:06 -06:00: INFO: 127.0.0.1:42694 - "GET /api/admin/users?skip=0&limit=20&search=&role=&status= HTTP/1.1" 200 OK
2026-02-01 20:23:08 -06:00: INFO: 127.0.0.1:42694 - "OPTIONS /api/admin/reports/sales?period=monthly HTTP/1.1" 200 OK
2026-02-01 20:23:08 -06:00: INFO: 127.0.0.1:42694 - "GET /api/admin/reports/sales?period=monthly HTTP/1.1" 200 OK
2026-02-01 20:23:19 -06:00: INFO: 127.0.0.1:50722 - "OPTIONS /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 20:23:19 -06:00: INFO: 127.0.0.1:50722 - "GET /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 20:23:27 -06:00: INFO: 127.0.0.1:50732 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:14:23 -06:00: INFO: 127.0.0.1:52242 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 21:14:23 -06:00: INFO: 127.0.0.1:52248 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:14:23 -06:00: INFO: 127.0.0.1:52256 - "PUT /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/complete HTTP/1.1" 200 OK
2026-02-01 21:14:33 -06:00: INFO: 127.0.0.1:34040 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 21:14:33 -06:00: INFO: 127.0.0.1:34050 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:14:33 -06:00: INFO: 127.0.0.1:34062 - "GET /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:14:54 -06:00: INFO: 127.0.0.1:5066 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 21:14:54 -06:00: INFO: 127.0.0.1:5068 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:14:54 -06:00: INFO: 127.0.0.1:5066 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 21:14:54 -06:00: INFO: 127.0.0.1:5068 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:14:54 -06:00: INFO: 127.0.0.1:5068 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:14:54 -06:00: INFO: 127.0.0.1:5066 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:14:54 -06:00: INFO: 127.0.0.1:5066 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:14:54 -06:00: INFO: 127.0.0.1:5084 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:14:56 -06:00: INFO: 127.0.0.1:5084 - "OPTIONS /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:14:56 -06:00: INFO: 127.0.0.1:5084 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:15:07 -06:00: INFO: 127.0.0.1:5090 - "OPTIONS /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:15:07 -06:00: INFO: 127.0.0.1:5090 - "GET /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:16:34 -06:00: INFO: 127.0.0.1:31348 - "GET /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:22:56 -06:00: INFO: 127.0.0.1:54392 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 21:22:56 -06:00: INFO: 127.0.0.1:54400 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:22:56 -06:00: INFO: 127.0.0.1:54392 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 21:22:56 -06:00: INFO: 127.0.0.1:54400 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:22:56 -06:00: INFO: 127.0.0.1:54392 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:22:56 -06:00: INFO: 127.0.0.1:54400 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:22:59 -06:00: INFO: 127.0.0.1:54400 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:23:04 -06:00: INFO: 127.0.0.1:54400 - "GET /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:25:26 -06:00: INFO: 127.0.0.1:62462 - "OPTIONS /api/admin/bookings/703b913e-20d7-4356-99f0-b33b8b28a89c/status?status=in-progress HTTP/1.1" 200 OK
2026-02-01 21:25:26 -06:00: INFO: 127.0.0.1:62462 - "PUT /api/admin/bookings/703b913e-20d7-4356-99f0-b33b8b28a89c/status?status=in-progress HTTP/1.1" 200 OK
2026-02-01 21:25:26 -06:00: INFO: 127.0.0.1:62462 - "OPTIONS /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:25:26 -06:00: INFO: 127.0.0.1:62476 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:25:44 -06:00: INFO: 127.0.0.1:57300 - "OPTIONS /api/admin/reports/sales?period=monthly HTTP/1.1" 200 OK
2026-02-01 21:25:44 -06:00: INFO: 127.0.0.1:57300 - "GET /api/admin/reports/sales?period=monthly HTTP/1.1" 200 OK
2026-02-01 21:25:50 -06:00: INFO: 127.0.0.1:57310 - "OPTIONS /api/admin/inventory HTTP/1.1" 200 OK
2026-02-01 21:25:50 -06:00: INFO: 127.0.0.1:57310 - "GET /api/admin/inventory HTTP/1.1" 200 OK
2026-02-01 21:25:54 -06:00: INFO: 127.0.0.1:57310 - "OPTIONS /api/admin/orders HTTP/1.1" 200 OK
2026-02-01 21:25:55 -06:00: INFO: 127.0.0.1:8022 - "GET /api/admin/orders HTTP/1.1" 200 OK
2026-02-01 21:25:55 -06:00: INFO: 127.0.0.1:8022 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:25:56 -06:00: INFO: 127.0.0.1:8022 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:25:56 -06:00: INFO: 127.0.0.1:8022 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:25:57 -06:00: INFO: 127.0.0.1:8022 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:27:02 -06:00: INFO: 127.0.0.1:60670 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 21:27:02 -06:00: INFO: 127.0.0.1:60672 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:27:03 -06:00: INFO: 127.0.0.1:60670 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 21:27:03 -06:00: INFO: 127.0.0.1:60672 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:27:03 -06:00: INFO: 127.0.0.1:60672 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:27:03 -06:00: INFO: 127.0.0.1:60670 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:27:03 -06:00: INFO: 127.0.0.1:60670 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:27:09 -06:00: INFO: 127.0.0.1:60680 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:27:44 -06:00: INFO: 127.0.0.1:60696 - "OPTIONS /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:27:44 -06:00: INFO: 127.0.0.1:60696 - "GET /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:28:49 -06:00: INFO: 127.0.0.1:20266 - "GET /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:31:01 -06:00: INFO: 127.0.0.1:61196 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:31:01 -06:00: INFO: 127.0.0.1:61210 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 21:31:01 -06:00: INFO: 127.0.0.1:61196 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:31:01 -06:00: INFO: 127.0.0.1:61210 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 21:31:01 -06:00: INFO: 127.0.0.1:61210 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:31:01 -06:00: INFO: 127.0.0.1:61196 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:31:03 -06:00: INFO: 127.0.0.1:61196 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:31:10 -06:00: INFO: 127.0.0.1:61216 - "OPTIONS /api/admin/bookings/703b913e-20d7-4356-99f0-b33b8b28a89c HTTP/1.1" 200 OK
2026-02-01 21:31:10 -06:00: INFO: 127.0.0.1:61216 - "DELETE /api/admin/bookings/703b913e-20d7-4356-99f0-b33b8b28a89c HTTP/1.1" 200 OK
2026-02-01 21:31:10 -06:00: INFO: 127.0.0.1:61216 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:32:59 -06:00: INFO: 127.0.0.1:23826 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 21:32:59 -06:00: INFO: 127.0.0.1:23840 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:32:59 -06:00: INFO: 127.0.0.1:23840 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:32:59 -06:00: INFO: 127.0.0.1:23826 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 21:32:59 -06:00: INFO: 127.0.0.1:23840 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:32:59 -06:00: INFO: 127.0.0.1:23826 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:33:01 -06:00: INFO: 127.0.0.1:23842 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:33:03 -06:00: INFO: 127.0.0.1:23842 - "GET /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:35:00 -06:00: INFO: 127.0.0.1:57440 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 21:35:01 -06:00: INFO: 127.0.0.1:57472 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 21:35:02 -06:00: INFO: 127.0.0.1:57440 - "GET /api/services/e9fdd79e-7728-4ca5-a961-b9aa2be10124 HTTP/1.1" 200 OK
2026-02-01 21:35:33 -06:00: INFO: 127.0.0.1:57456 - "GET /api/services/e9fdd79e-7728-4ca5-a961-b9aa2be10124 HTTP/1.1" 200 OK
2026-02-01 21:35:33 -06:00: INFO: 127.0.0.1:57456 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 21:35:33 -06:00: INFO: 127.0.0.1:2446 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:35:34 -06:00: INFO: 127.0.0.1:2454 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 21:35:34 -06:00: INFO: 127.0.0.1:2446 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:35:35 -06:00: INFO: 127.0.0.1:2454 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:35:35 -06:00: INFO: 127.0.0.1:2446 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:35:39 -06:00: INFO: 127.0.0.1:2446 - "OPTIONS /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:35:39 -06:00: INFO: 127.0.0.1:2446 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:35:40 -06:00: INFO: 127.0.0.1:2446 - "GET /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:37:59 -06:00: INFO: 127.0.0.1:25062 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:37:59 -06:00: INFO: 127.0.0.1:25072 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 21:37:59 -06:00: INFO: 127.0.0.1:25062 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:37:59 -06:00: INFO: 127.0.0.1:25072 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 21:37:59 -06:00: INFO: 127.0.0.1:25072 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:37:59 -06:00: INFO: 127.0.0.1:25062 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 21:37:59 -06:00: INFO: 127.0.0.1:25062 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:37:59 -06:00: INFO: 127.0.0.1:25072 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:37:59 -06:00: INFO: 127.0.0.1:25062 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:37:59 -06:00: INFO: 127.0.0.1:25072 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:38:01 -06:00: INFO: 127.0.0.1:25072 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:38:02 -06:00: INFO: 127.0.0.1:25072 - "OPTIONS /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:38:02 -06:00: INFO: 127.0.0.1:25072 - "GET /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:39:16 -06:00: INFO: 127.0.0.1:58864 - "OPTIONS /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db HTTP/1.1" 200 OK
2026-02-01 21:39:16 -06:00: INFO: 127.0.0.1:58864 - "DELETE /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db HTTP/1.1" 200 OK
2026-02-01 21:39:16 -06:00: INFO: 127.0.0.1:58864 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:39:18 -06:00: INFO: 127.0.0.1:58864 - "OPTIONS /api/admin/bookings/2ca83d28-a948-4365-a784-7f6b834f1cc3 HTTP/1.1" 200 OK
2026-02-01 21:39:18 -06:00: INFO: 127.0.0.1:58864 - "DELETE /api/admin/bookings/2ca83d28-a948-4365-a784-7f6b834f1cc3 HTTP/1.1" 200 OK
2026-02-01 21:39:18 -06:00: INFO: 127.0.0.1:58864 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:39:20 -06:00: INFO: 127.0.0.1:58864 - "OPTIONS /api/admin/reports/sales?period=monthly HTTP/1.1" 200 OK
2026-02-01 21:39:20 -06:00: INFO: 127.0.0.1:58864 - "GET /api/admin/reports/sales?period=monthly HTTP/1.1" 200 OK
2026-02-01 21:41:34 -06:00: INFO: 127.0.0.1:42058 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 21:41:34 -06:00: INFO: 127.0.0.1:42058 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 21:41:36 -06:00: INFO: 127.0.0.1:42066 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 21:41:37 -06:00: INFO: 127.0.0.1:42058 - "GET /api/services/e9fdd79e-7728-4ca5-a961-b9aa2be10124 HTTP/1.1" 200 OK
2026-02-01 21:41:50 -06:00: INFO: 127.0.0.1:5684 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:41:50 -06:00: INFO: 127.0.0.1:5676 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:42:19 -06:00: INFO: 127.0.0.1:57242 - "OPTIONS /api/admin/inventory HTTP/1.1" 200 OK
2026-02-01 21:42:19 -06:00: INFO: 127.0.0.1:57242 - "GET /api/admin/inventory HTTP/1.1" 200 OK
2026-02-01 21:46:21 -06:00: INFO: 127.0.0.1:52242 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 21:46:21 -06:00: INFO: 127.0.0.1:52248 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:46:21 -06:00: INFO: 127.0.0.1:52242 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 21:46:21 -06:00: INFO: 127.0.0.1:52248 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:46:22 -06:00: INFO: 127.0.0.1:52242 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:46:22 -06:00: INFO: 127.0.0.1:52248 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:46:22 -06:00: INFO: 127.0.0.1:52248 - "OPTIONS /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:46:23 -06:00: INFO: 127.0.0.1:52248 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:48:12 -06:00: INFO: 127.0.0.1:32150 - "OPTIONS /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 21:48:12 -06:00: INFO: 127.0.0.1:32150 - "GET /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 21:48:12 -06:00: INFO: 127.0.0.1:32168 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 304 Not Modified
2026-02-01 21:48:21 -06:00: INFO: 127.0.0.1:18696 - "OPTIONS /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 21:48:21 -06:00: INFO: 127.0.0.1:18696 - "GET /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 21:48:58 -06:00: INFO: 127.0.0.1:53088 - "OPTIONS /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 21:48:59 -06:00: INFO: 127.0.0.1:53098 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 21:48:59 -06:00: INFO: 127.0.0.1:53122 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 21:49:09 -06:00: INFO: 127.0.0.1:53110 - "OPTIONS /api/admin/services/7a2cd09a-e8fe-4360-b006-aeafa1c23fa5 HTTP/1.1" 200 OK
2026-02-01 21:49:09 -06:00: INFO: 127.0.0.1:53110 - "DELETE /api/admin/services/7a2cd09a-e8fe-4360-b006-aeafa1c23fa5 HTTP/1.1" 200 OK
2026-02-01 21:49:09 -06:00: INFO: 127.0.0.1:53110 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 21:49:13 -06:00: INFO: 127.0.0.1:53110 - "OPTIONS /api/admin/services/528d18a7-e24f-432e-bf1d-842f534c44f0 HTTP/1.1" 200 OK
2026-02-01 21:49:13 -06:00: INFO: 127.0.0.1:53110 - "DELETE /api/admin/services/528d18a7-e24f-432e-bf1d-842f534c44f0 HTTP/1.1" 200 OK
2026-02-01 21:49:13 -06:00: INFO: 127.0.0.1:53110 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 21:49:38 -06:00: INFO: 127.0.0.1:19530 - "OPTIONS /api/admin/services/9c70d282-8b80-45d0-ba38-e93bd8f0479c HTTP/1.1" 200 OK
2026-02-01 21:49:38 -06:00: INFO: 127.0.0.1:19530 - "PUT /api/admin/services/9c70d282-8b80-45d0-ba38-e93bd8f0479c HTTP/1.1" 500 Internal Server Error
2026-02-01 22:03:42 -06:00: INFO: 127.0.0.1:45878 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 22:03:42 -06:00: INFO: 127.0.0.1:45886 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 22:03:42 -06:00: INFO: 127.0.0.1:45886 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 22:03:42 -06:00: INFO: 127.0.0.1:45886 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 22:03:42 -06:00: INFO: 127.0.0.1:45878 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 22:03:42 -06:00: INFO: 127.0.0.1:45878 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 22:03:42 -06:00: INFO: 127.0.0.1:45878 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 22:03:42 -06:00: INFO: 127.0.0.1:45890 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 22:03:45 -06:00: INFO: 127.0.0.1:45878 - "OPTIONS /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:03:45 -06:00: INFO: 127.0.0.1:45878 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:04:30 -06:00: INFO: 127.0.0.1:45916 - "OPTIONS /api/admin/services/9c70d282-8b80-45d0-ba38-e93bd8f0479c HTTP/1.1" 200 OK
2026-02-01 22:04:30 -06:00: INFO: 127.0.0.1:45916 - "PUT /api/admin/services/9c70d282-8b80-45d0-ba38-e93bd8f0479c HTTP/1.1" 200 OK
2026-02-01 22:04:30 -06:00: INFO: 127.0.0.1:45916 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:06:26 -06:00: INFO: 127.0.0.1:59192 - "OPTIONS /api/admin/services/d1633bbb-dc79-400e-95ce-2c91554d58e8 HTTP/1.1" 200 OK
2026-02-01 22:06:26 -06:00: INFO: 127.0.0.1:59192 - "PUT /api/admin/services/d1633bbb-dc79-400e-95ce-2c91554d58e8 HTTP/1.1" 200 OK
2026-02-01 22:06:26 -06:00: INFO: 127.0.0.1:59192 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:10:24 -06:00: INFO: 127.0.0.1:55494 - "OPTIONS /api/admin/services/e9fdd79e-7728-4ca5-a961-b9aa2be10124 HTTP/1.1" 200 OK
2026-02-01 22:10:25 -06:00: INFO: 127.0.0.1:55508 - "PUT /api/admin/services/e9fdd79e-7728-4ca5-a961-b9aa2be10124 HTTP/1.1" 200 OK
2026-02-01 22:10:25 -06:00: INFO: 127.0.0.1:55508 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:11:27 -06:00: INFO: 127.0.0.1:43970 - "OPTIONS /api/admin/services/fa6c750f-f1c1-406f-bf16-769106d2bfbf HTTP/1.1" 200 OK
2026-02-01 22:11:27 -06:00: INFO: 127.0.0.1:43972 - "PUT /api/admin/services/fa6c750f-f1c1-406f-bf16-769106d2bfbf HTTP/1.1" 200 OK
2026-02-01 22:11:27 -06:00: INFO: 127.0.0.1:43972 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:12:46 -06:00: INFO: 127.0.0.1:63840 - "PUT /api/admin/services/fa6c750f-f1c1-406f-bf16-769106d2bfbf HTTP/1.1" 200 OK
2026-02-01 22:12:46 -06:00: INFO: 127.0.0.1:63840 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:12:52 -06:00: INFO: 127.0.0.1:63856 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 22:12:52 -06:00: INFO: 127.0.0.1:63856 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 22:12:52 -06:00: INFO: 127.0.0.1:63856 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 22:12:52 -06:00: INFO: 127.0.0.1:63856 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 22:12:54 -06:00: INFO: 127.0.0.1:63856 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 22:13:30 -06:00: INFO: 127.0.0.1:6322 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 22:13:30 -06:00: INFO: 127.0.0.1:6320 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 22:13:32 -06:00: INFO: 127.0.0.1:6320 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:13:58 -06:00: INFO: 127.0.0.1:11176 - "GET /api/services/9c70d282-8b80-45d0-ba38-e93bd8f0479c HTTP/1.1" 200 OK
2026-02-01 22:14:05 -06:00: INFO: 127.0.0.1:11186 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 22:14:05 -06:00: INFO: 127.0.0.1:11186 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 22:14:06 -06:00: INFO: 127.0.0.1:55478 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 22:14:06 -06:00: INFO: 127.0.0.1:55464 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 22:14:09 -06:00: INFO: 127.0.0.1:55464 - "OPTIONS /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:14:09 -06:00: INFO: 127.0.0.1:55464 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:14:21 -06:00: INFO: 127.0.0.1:55490 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 22:14:25 -06:00: INFO: 127.0.0.1:55490 - "GET /api/services/e9fdd79e-7728-4ca5-a961-b9aa2be10124 HTTP/1.1" 200 OK
2026-02-01 22:19:04 -06:00: INFO: 127.0.0.1:46468 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 22:19:05 -06:00: INFO: 127.0.0.1:46482 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 22:19:05 -06:00: INFO: 127.0.0.1:46468 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 22:19:05 -06:00: INFO: 127.0.0.1:46486 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 22:19:05 -06:00: INFO: 127.0.0.1:46482 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 22:19:09 -06:00: INFO: 127.0.0.1:46482 - "GET /api/services/e9fdd79e-7728-4ca5-a961-b9aa2be10124 HTTP/1.1" 200 OK
2026-02-01 22:19:20 -06:00: INFO: 127.0.0.1:46496 - "GET /api/services/d1633bbb-dc79-400e-95ce-2c91554d58e8 HTTP/1.1" 200 OK
2026-02-01 22:19:23 -06:00: INFO: 127.0.0.1:46496 - "GET /api/services/fff97750-c6ee-4350-80c0-87cf93f16c13 HTTP/1.1" 200 OK
2026-02-01 22:20:03 -06:00: INFO: 127.0.0.1:11218 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 22:20:03 -06:00: INFO: 127.0.0.1:11228 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 22:20:07 -06:00: INFO: 127.0.0.1:11228 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:21:20 -06:00: INFO: 127.0.0.1:19538 - "OPTIONS /api/admin/services/fff97750-c6ee-4350-80c0-87cf93f16c13 HTTP/1.1" 200 OK
2026-02-01 22:21:20 -06:00: INFO: 127.0.0.1:19538 - "PUT /api/admin/services/fff97750-c6ee-4350-80c0-87cf93f16c13 HTTP/1.1" 200 OK
2026-02-01 22:21:20 -06:00: INFO: 127.0.0.1:19538 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:22:31 -06:00: INFO: 127.0.0.1:24738 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 22:23:17 -06:00: INFO: 127.0.0.1:27426 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 22:23:47 -06:00: INFO: 127.0.0.1:47310 - "POST /api/auth/login HTTP/1.1" 401 Unauthorized
2026-02-01 22:24:02 -06:00: INFO: 127.0.0.1:45828 - "POST /api/auth/login HTTP/1.1" 401 Unauthorized
2026-02-01 22:25:26 -06:00: INFO: 127.0.0.1:23242 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 22:25:41 -06:00: INFO: 127.0.0.1:60338 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 22:25:41 -06:00: INFO: 127.0.0.1:30518 - "PUT /api/admin/services/fa6c750f-f1c1-406f-bf16-769106d2bfbf HTTP/1.1" 200 OK
2026-02-01 22:28:27 -06:00: INFO: 127.0.0.1:12810 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 22:28:27 -06:00: INFO: 127.0.0.1:12824 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 22:28:27 -06:00: INFO: 127.0.0.1:12824 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 22:28:27 -06:00: INFO: 127.0.0.1:12824 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 22:28:27 -06:00: INFO: 127.0.0.1:12824 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 22:28:27 -06:00: INFO: 127.0.0.1:12828 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 22:28:27 -06:00: INFO: 127.0.0.1:12828 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 22:28:27 -06:00: INFO: 127.0.0.1:12828 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 22:28:30 -06:00: INFO: 127.0.0.1:12828 - "OPTIONS /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:28:30 -06:00: INFO: 127.0.0.1:12828 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:28:44 -06:00: INFO: 127.0.0.1:12838 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 22:28:54 -06:00: INFO: 127.0.0.1:12856 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 22:28:54 -06:00: INFO: 127.0.0.1:12852 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 22:28:58 -06:00: INFO: 127.0.0.1:12852 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:29:14 -06:00: INFO: 127.0.0.1:12870 - "OPTIONS /api/admin/services/fa6c750f-f1c1-406f-bf16-769106d2bfbf HTTP/1.1" 200 OK
2026-02-01 22:29:14 -06:00: INFO: 127.0.0.1:12870 - "PUT /api/admin/services/fa6c750f-f1c1-406f-bf16-769106d2bfbf HTTP/1.1" 200 OK
2026-02-01 22:29:14 -06:00: INFO: 127.0.0.1:12870 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:29:28 -06:00: INFO: 127.0.0.1:39314 - "OPTIONS /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:29:29 -06:00: INFO: 127.0.0.1:39314 - "GET /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:29:29 -06:00: INFO: 127.0.0.1:39314 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:29:30 -06:00: INFO: 127.0.0.1:39314 - "GET /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:29:35 -06:00: INFO: 127.0.0.1:39328 - "GET /api/products HTTP/1.1" 200 OK

View File

@@ -54,3 +54,133 @@
2026-01-13 18:37:05 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-01-13 18:37:05 -06:00: (node:516319) [DEP_WEBPACK_DEV_SERVER_ON_AFTER_SETUP_MIDDLEWARE] DeprecationWarning: 'onAfterSetupMiddleware' option is deprecated. Please use the 'setupMiddlewares' option.
2026-01-13 18:37:05 -06:00: (node:516319) [DEP_WEBPACK_DEV_SERVER_ON_BEFORE_SETUP_MIDDLEWARE] DeprecationWarning: 'onBeforeSetupMiddleware' option is deprecated. Please use the 'setupMiddlewares' option.
2026-02-01 16:28:57 -06:00: (node:305102) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:28:57 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:28:58 -06:00: (node:305232) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:28:58 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:28:59 -06:00: (node:305267) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:28:59 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:00 -06:00: (node:305324) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:00 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:00 -06:00: (node:305380) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:00 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:01 -06:00: (node:305445) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:01 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:02 -06:00: (node:305732) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:02 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:03 -06:00: (node:305767) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:03 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:04 -06:00: (node:305804) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:04 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:05 -06:00: (node:305840) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:05 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:06 -06:00: (node:305878) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:06 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:06 -06:00: (node:305921) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:06 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:07 -06:00: (node:306039) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:07 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:08 -06:00: (node:306179) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:08 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:09 -06:00: (node:306554) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:09 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:10 -06:00: (node:306598) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:10 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:11 -06:00: (node:306668) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:11 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:41 -06:00: (node:310173) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:41 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:42 -06:00: (node:310223) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:42 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:43 -06:00: (node:310327) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:43 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:44 -06:00: (node:310388) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:44 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:45 -06:00: (node:310423) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:45 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:46 -06:00: (node:310461) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:46 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:47 -06:00: (node:310507) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:47 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:48 -06:00: (node:310551) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:48 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:48 -06:00: (node:310647) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:48 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:49 -06:00: (node:310704) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:49 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:50 -06:00: (node:310739) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:50 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:51 -06:00: (node:310778) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:51 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:52 -06:00: (node:310839) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:52 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:53 -06:00: (node:310879) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:53 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:53 -06:00: (node:310972) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:53 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:54 -06:00: (node:311031) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:54 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:44 -06:00: (node:317523) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:44 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:45 -06:00: (node:317636) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:45 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:46 -06:00: (node:317670) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:46 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:47 -06:00: (node:317750) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:47 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:48 -06:00: (node:317784) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:48 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:48 -06:00: (node:317822) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:48 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:49 -06:00: (node:317857) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:49 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:50 -06:00: (node:317970) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:50 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:51 -06:00: (node:318021) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:51 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:52 -06:00: (node:318088) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:52 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:53 -06:00: (node:318141) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:53 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:54 -06:00: (node:318179) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:54 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:55 -06:00: (node:318215) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:55 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:55 -06:00: (node:318313) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:55 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:56 -06:00: (node:318347) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:56 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:57 -06:00: (node:318412) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:57 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:22 -06:00: (node:322333) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:22 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:23 -06:00: (node:322412) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:23 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:24 -06:00: (node:322461) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:24 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:25 -06:00: (node:322506) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:25 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:26 -06:00: (node:322544) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:26 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:27 -06:00: (node:322649) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:27 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:28 -06:00: (node:322687) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:28 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:29 -06:00: (node:322741) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:29 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:29 -06:00: (node:322780) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:29 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:30 -06:00: (node:322814) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:30 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:31 -06:00: (node:322911) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:31 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:32 -06:00: (node:322953) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:32 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:33 -06:00: (node:322991) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:33 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:34 -06:00: (node:323063) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:34 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:35 -06:00: (node:323103) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:35 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:35 -06:00: (node:323138) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:35 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)

View File

@@ -1102,3 +1102,328 @@
2026-01-13 18:37:07 -06:00: cached modules 5.02 MiB [cached] 1940 modules
2026-01-13 18:37:07 -06:00: runtime modules 29.2 KiB 15 modules
2026-01-13 18:37:07 -06:00: webpack 5.104.1 compiled successfully in 1353 ms
2026-02-01 16:28:55 -06:00:
2026-02-01 16:28:55 -06:00: > frontend@0.1.0 start
2026-02-01 16:28:55 -06:00: > craco start
2026-02-01 16:28:55 -06:00:
2026-02-01 16:28:57 -06:00: Something is already running on port 5300.
2026-02-01 16:28:57 -06:00:
2026-02-01 16:28:57 -06:00: > frontend@0.1.0 start
2026-02-01 16:28:57 -06:00: > craco start
2026-02-01 16:28:57 -06:00:
2026-02-01 16:28:58 -06:00: Something is already running on port 5300.
2026-02-01 16:28:58 -06:00:
2026-02-01 16:28:58 -06:00: > frontend@0.1.0 start
2026-02-01 16:28:58 -06:00: > craco start
2026-02-01 16:28:58 -06:00:
2026-02-01 16:28:59 -06:00: Something is already running on port 5300.
2026-02-01 16:28:59 -06:00:
2026-02-01 16:28:59 -06:00: > frontend@0.1.0 start
2026-02-01 16:28:59 -06:00: > craco start
2026-02-01 16:28:59 -06:00:
2026-02-01 16:29:00 -06:00: Something is already running on port 5300.
2026-02-01 16:29:00 -06:00:
2026-02-01 16:29:00 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:00 -06:00: > craco start
2026-02-01 16:29:00 -06:00:
2026-02-01 16:29:00 -06:00: Something is already running on port 5300.
2026-02-01 16:29:01 -06:00:
2026-02-01 16:29:01 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:01 -06:00: > craco start
2026-02-01 16:29:01 -06:00:
2026-02-01 16:29:01 -06:00: Something is already running on port 5300.
2026-02-01 16:29:01 -06:00:
2026-02-01 16:29:01 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:01 -06:00: > craco start
2026-02-01 16:29:01 -06:00:
2026-02-01 16:29:02 -06:00: Something is already running on port 5300.
2026-02-01 16:29:02 -06:00:
2026-02-01 16:29:02 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:02 -06:00: > craco start
2026-02-01 16:29:02 -06:00:
2026-02-01 16:29:03 -06:00: Something is already running on port 5300.
2026-02-01 16:29:03 -06:00:
2026-02-01 16:29:03 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:03 -06:00: > craco start
2026-02-01 16:29:03 -06:00:
2026-02-01 16:29:04 -06:00: Something is already running on port 5300.
2026-02-01 16:29:04 -06:00:
2026-02-01 16:29:04 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:04 -06:00: > craco start
2026-02-01 16:29:04 -06:00:
2026-02-01 16:29:05 -06:00: Something is already running on port 5300.
2026-02-01 16:29:05 -06:00:
2026-02-01 16:29:05 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:05 -06:00: > craco start
2026-02-01 16:29:05 -06:00:
2026-02-01 16:29:06 -06:00: Something is already running on port 5300.
2026-02-01 16:29:06 -06:00:
2026-02-01 16:29:06 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:06 -06:00: > craco start
2026-02-01 16:29:06 -06:00:
2026-02-01 16:29:06 -06:00: Something is already running on port 5300.
2026-02-01 16:29:07 -06:00:
2026-02-01 16:29:07 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:07 -06:00: > craco start
2026-02-01 16:29:07 -06:00:
2026-02-01 16:29:07 -06:00: Something is already running on port 5300.
2026-02-01 16:29:07 -06:00:
2026-02-01 16:29:07 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:07 -06:00: > craco start
2026-02-01 16:29:07 -06:00:
2026-02-01 16:29:08 -06:00: Something is already running on port 5300.
2026-02-01 16:29:08 -06:00:
2026-02-01 16:29:08 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:08 -06:00: > craco start
2026-02-01 16:29:08 -06:00:
2026-02-01 16:29:09 -06:00: Something is already running on port 5300.
2026-02-01 16:29:09 -06:00:
2026-02-01 16:29:09 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:09 -06:00: > craco start
2026-02-01 16:29:09 -06:00:
2026-02-01 16:29:10 -06:00: Something is already running on port 5300.
2026-02-01 16:29:10 -06:00:
2026-02-01 16:29:10 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:10 -06:00: > craco start
2026-02-01 16:29:10 -06:00:
2026-02-01 16:29:11 -06:00: Something is already running on port 5300.
2026-02-01 16:30:41 -06:00:
2026-02-01 16:30:41 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:41 -06:00: > craco start
2026-02-01 16:30:41 -06:00:
2026-02-01 16:30:41 -06:00: Something is already running on port 5300.
2026-02-01 16:30:42 -06:00:
2026-02-01 16:30:42 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:42 -06:00: > craco start
2026-02-01 16:30:42 -06:00:
2026-02-01 16:30:42 -06:00: Something is already running on port 5300.
2026-02-01 16:30:42 -06:00:
2026-02-01 16:30:42 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:42 -06:00: > craco start
2026-02-01 16:30:42 -06:00:
2026-02-01 16:30:43 -06:00: Something is already running on port 5300.
2026-02-01 16:30:43 -06:00:
2026-02-01 16:30:43 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:43 -06:00: > craco start
2026-02-01 16:30:43 -06:00:
2026-02-01 16:30:44 -06:00: Something is already running on port 5300.
2026-02-01 16:30:44 -06:00:
2026-02-01 16:30:44 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:44 -06:00: > craco start
2026-02-01 16:30:44 -06:00:
2026-02-01 16:30:45 -06:00: Something is already running on port 5300.
2026-02-01 16:30:45 -06:00:
2026-02-01 16:30:45 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:45 -06:00: > craco start
2026-02-01 16:30:45 -06:00:
2026-02-01 16:30:46 -06:00: Something is already running on port 5300.
2026-02-01 16:30:46 -06:00:
2026-02-01 16:30:46 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:46 -06:00: > craco start
2026-02-01 16:30:46 -06:00:
2026-02-01 16:30:47 -06:00: Something is already running on port 5300.
2026-02-01 16:30:47 -06:00:
2026-02-01 16:30:47 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:47 -06:00: > craco start
2026-02-01 16:30:47 -06:00:
2026-02-01 16:30:48 -06:00: Something is already running on port 5300.
2026-02-01 16:30:48 -06:00:
2026-02-01 16:30:48 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:48 -06:00: > craco start
2026-02-01 16:30:48 -06:00:
2026-02-01 16:30:48 -06:00: Something is already running on port 5300.
2026-02-01 16:30:48 -06:00:
2026-02-01 16:30:48 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:48 -06:00: > craco start
2026-02-01 16:30:48 -06:00:
2026-02-01 16:30:49 -06:00: Something is already running on port 5300.
2026-02-01 16:30:49 -06:00:
2026-02-01 16:30:49 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:49 -06:00: > craco start
2026-02-01 16:30:49 -06:00:
2026-02-01 16:30:50 -06:00: Something is already running on port 5300.
2026-02-01 16:30:50 -06:00:
2026-02-01 16:30:50 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:50 -06:00: > craco start
2026-02-01 16:30:50 -06:00:
2026-02-01 16:30:51 -06:00: Something is already running on port 5300.
2026-02-01 16:30:51 -06:00:
2026-02-01 16:30:51 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:51 -06:00: > craco start
2026-02-01 16:30:51 -06:00:
2026-02-01 16:30:52 -06:00: Something is already running on port 5300.
2026-02-01 16:30:52 -06:00:
2026-02-01 16:30:52 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:52 -06:00: > craco start
2026-02-01 16:30:52 -06:00:
2026-02-01 16:30:53 -06:00: Something is already running on port 5300.
2026-02-01 16:30:53 -06:00:
2026-02-01 16:30:53 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:53 -06:00: > craco start
2026-02-01 16:30:53 -06:00:
2026-02-01 16:30:53 -06:00: Something is already running on port 5300.
2026-02-01 16:30:54 -06:00:
2026-02-01 16:30:54 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:54 -06:00: > craco start
2026-02-01 16:30:54 -06:00:
2026-02-01 16:30:54 -06:00: Something is already running on port 5300.
2026-02-01 16:33:43 -06:00:
2026-02-01 16:33:43 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:43 -06:00: > craco start
2026-02-01 16:33:43 -06:00:
2026-02-01 16:33:44 -06:00: Something is already running on port 5300.
2026-02-01 16:33:44 -06:00:
2026-02-01 16:33:44 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:44 -06:00: > craco start
2026-02-01 16:33:44 -06:00:
2026-02-01 16:33:45 -06:00: Something is already running on port 5300.
2026-02-01 16:33:45 -06:00:
2026-02-01 16:33:45 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:45 -06:00: > craco start
2026-02-01 16:33:45 -06:00:
2026-02-01 16:33:46 -06:00: Something is already running on port 5300.
2026-02-01 16:33:46 -06:00:
2026-02-01 16:33:46 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:46 -06:00: > craco start
2026-02-01 16:33:46 -06:00:
2026-02-01 16:33:47 -06:00: Something is already running on port 5300.
2026-02-01 16:33:47 -06:00:
2026-02-01 16:33:47 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:47 -06:00: > craco start
2026-02-01 16:33:47 -06:00:
2026-02-01 16:33:48 -06:00: Something is already running on port 5300.
2026-02-01 16:33:48 -06:00:
2026-02-01 16:33:48 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:48 -06:00: > craco start
2026-02-01 16:33:48 -06:00:
2026-02-01 16:33:48 -06:00: Something is already running on port 5300.
2026-02-01 16:33:49 -06:00:
2026-02-01 16:33:49 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:49 -06:00: > craco start
2026-02-01 16:33:49 -06:00:
2026-02-01 16:33:49 -06:00: Something is already running on port 5300.
2026-02-01 16:33:49 -06:00:
2026-02-01 16:33:49 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:49 -06:00: > craco start
2026-02-01 16:33:49 -06:00:
2026-02-01 16:33:50 -06:00: Something is already running on port 5300.
2026-02-01 16:33:50 -06:00:
2026-02-01 16:33:50 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:50 -06:00: > craco start
2026-02-01 16:33:50 -06:00:
2026-02-01 16:33:51 -06:00: Something is already running on port 5300.
2026-02-01 16:33:51 -06:00:
2026-02-01 16:33:51 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:51 -06:00: > craco start
2026-02-01 16:33:51 -06:00:
2026-02-01 16:33:52 -06:00: Something is already running on port 5300.
2026-02-01 16:33:52 -06:00:
2026-02-01 16:33:52 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:52 -06:00: > craco start
2026-02-01 16:33:52 -06:00:
2026-02-01 16:33:53 -06:00: Something is already running on port 5300.
2026-02-01 16:33:53 -06:00:
2026-02-01 16:33:53 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:53 -06:00: > craco start
2026-02-01 16:33:53 -06:00:
2026-02-01 16:33:54 -06:00: Something is already running on port 5300.
2026-02-01 16:33:54 -06:00:
2026-02-01 16:33:54 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:54 -06:00: > craco start
2026-02-01 16:33:54 -06:00:
2026-02-01 16:33:55 -06:00: Something is already running on port 5300.
2026-02-01 16:33:55 -06:00:
2026-02-01 16:33:55 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:55 -06:00: > craco start
2026-02-01 16:33:55 -06:00:
2026-02-01 16:33:55 -06:00: Something is already running on port 5300.
2026-02-01 16:33:55 -06:00:
2026-02-01 16:33:55 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:55 -06:00: > craco start
2026-02-01 16:33:55 -06:00:
2026-02-01 16:33:56 -06:00: Something is already running on port 5300.
2026-02-01 16:33:56 -06:00:
2026-02-01 16:33:56 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:56 -06:00: > craco start
2026-02-01 16:33:56 -06:00:
2026-02-01 16:33:57 -06:00: Something is already running on port 5300.
2026-02-01 16:35:22 -06:00:
2026-02-01 16:35:22 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:22 -06:00: > craco start
2026-02-01 16:35:22 -06:00:
2026-02-01 16:35:22 -06:00: Something is already running on port 5300.
2026-02-01 16:35:23 -06:00:
2026-02-01 16:35:23 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:23 -06:00: > craco start
2026-02-01 16:35:23 -06:00:
2026-02-01 16:35:23 -06:00: Something is already running on port 5300.
2026-02-01 16:35:23 -06:00:
2026-02-01 16:35:23 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:23 -06:00: > craco start
2026-02-01 16:35:23 -06:00:
2026-02-01 16:35:24 -06:00: Something is already running on port 5300.
2026-02-01 16:35:24 -06:00:
2026-02-01 16:35:24 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:24 -06:00: > craco start
2026-02-01 16:35:24 -06:00:
2026-02-01 16:35:25 -06:00: Something is already running on port 5300.
2026-02-01 16:35:25 -06:00:
2026-02-01 16:35:25 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:25 -06:00: > craco start
2026-02-01 16:35:25 -06:00:
2026-02-01 16:35:26 -06:00: Something is already running on port 5300.
2026-02-01 16:35:26 -06:00:
2026-02-01 16:35:26 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:26 -06:00: > craco start
2026-02-01 16:35:26 -06:00:
2026-02-01 16:35:27 -06:00: Something is already running on port 5300.
2026-02-01 16:35:27 -06:00:
2026-02-01 16:35:27 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:27 -06:00: > craco start
2026-02-01 16:35:27 -06:00:
2026-02-01 16:35:28 -06:00: Something is already running on port 5300.
2026-02-01 16:35:28 -06:00:
2026-02-01 16:35:28 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:28 -06:00: > craco start
2026-02-01 16:35:28 -06:00:
2026-02-01 16:35:29 -06:00: Something is already running on port 5300.
2026-02-01 16:35:29 -06:00:
2026-02-01 16:35:29 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:29 -06:00: > craco start
2026-02-01 16:35:29 -06:00:
2026-02-01 16:35:29 -06:00: Something is already running on port 5300.
2026-02-01 16:35:30 -06:00:
2026-02-01 16:35:30 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:30 -06:00: > craco start
2026-02-01 16:35:30 -06:00:
2026-02-01 16:35:30 -06:00: Something is already running on port 5300.
2026-02-01 16:35:30 -06:00:
2026-02-01 16:35:30 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:30 -06:00: > craco start
2026-02-01 16:35:30 -06:00:
2026-02-01 16:35:31 -06:00: Something is already running on port 5300.
2026-02-01 16:35:31 -06:00:
2026-02-01 16:35:31 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:31 -06:00: > craco start
2026-02-01 16:35:31 -06:00:
2026-02-01 16:35:32 -06:00: Something is already running on port 5300.
2026-02-01 16:35:32 -06:00:
2026-02-01 16:35:32 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:32 -06:00: > craco start
2026-02-01 16:35:32 -06:00:
2026-02-01 16:35:33 -06:00: Something is already running on port 5300.
2026-02-01 16:35:33 -06:00:
2026-02-01 16:35:33 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:33 -06:00: > craco start
2026-02-01 16:35:33 -06:00:
2026-02-01 16:35:34 -06:00: Something is already running on port 5300.
2026-02-01 16:35:34 -06:00:
2026-02-01 16:35:34 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:34 -06:00: > craco start
2026-02-01 16:35:34 -06:00:
2026-02-01 16:35:35 -06:00: Something is already running on port 5300.
2026-02-01 16:35:35 -06:00:
2026-02-01 16:35:35 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:35 -06:00: > craco start
2026-02-01 16:35:35 -06:00:
2026-02-01 16:35:35 -06:00: Something is already running on port 5300.

View File

@@ -8,6 +8,10 @@ server {
# Disable ModSecurity for this site
modsecurity off;
# Buffer sizes
large_client_header_buffers 4 32k;
client_header_buffer_size 8k;
# Logs
access_log /var/log/nginx/prompttech-access.log;
@@ -20,17 +24,18 @@ server {
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
# Proxy to React dev server
# Serve built React static files
root /media/pts/Website/PromptTech_Solution_Site/frontend/build;
index index.html index.htm;
location / {
proxy_pass http://127.0.0.1:5300;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
try_files $uri $uri/ /index.html;
}
# Static file caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# API proxy to FastAPI backend
@@ -53,4 +58,18 @@ server {
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
# Proxy uploads to backend (media files) - ^~ prevents regex match
location ^~ /uploads/ {
proxy_pass http://127.0.0.1:8181/uploads/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Cache uploaded media files
expires 1y;
add_header Cache-Control "public, immutable";
}
}

View File

@@ -0,0 +1,258 @@
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
Commands marked with * may be preceded by a number, _N.
Notes in parentheses indicate the behavior if _N is given.
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
h H Display this help.
q :q Q :Q ZZ Exit.
---------------------------------------------------------------------------
MMOOVVIINNGG
e ^E j ^N CR * Forward one line (or _N lines).
y ^Y k ^K ^P * Backward one line (or _N lines).
f ^F ^V SPACE * Forward one window (or _N lines).
b ^B ESC-v * Backward one window (or _N lines).
z * Forward one window (and set window to _N).
w * Backward one window (and set window to _N).
ESC-SPACE * Forward one window, but don't stop at end-of-file.
d ^D * Forward one half-window (and set half-window to _N).
u ^U * Backward one half-window (and set half-window to _N).
ESC-) RightArrow * Right one half screen width (or _N positions).
ESC-( LeftArrow * Left one half screen width (or _N positions).
ESC-} ^RightArrow Right to last column displayed.
ESC-{ ^LeftArrow Left to first column.
F Forward forever; like "tail -f".
ESC-F Like F but stop when search pattern is found.
r ^R ^L Repaint screen.
R Repaint screen, discarding buffered input.
---------------------------------------------------
Default "window" is the screen height.
Default "half-window" is half of the screen height.
---------------------------------------------------------------------------
SSEEAARRCCHHIINNGG
/_p_a_t_t_e_r_n * Search forward for (_N-th) matching line.
?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line.
n * Repeat previous search (for _N-th occurrence).
N * Repeat previous search in reverse direction.
ESC-n * Repeat previous search, spanning files.
ESC-N * Repeat previous search, reverse dir. & spanning files.
ESC-u Undo (toggle) search highlighting.
ESC-U Clear search highlighting.
&_p_a_t_t_e_r_n * Display only matching lines.
---------------------------------------------------
A search pattern may begin with one or more of:
^N or ! Search for NON-matching lines.
^E or * Search multiple files (pass thru END OF FILE).
^F or @ Start search at FIRST file (for /) or last file (for ?).
^K Highlight matches, but don't move (KEEP position).
^R Don't use REGULAR EXPRESSIONS.
^W WRAP search if no match found.
---------------------------------------------------------------------------
JJUUMMPPIINNGG
g < ESC-< * Go to first line in file (or line _N).
G > ESC-> * Go to last line in file (or line _N).
p % * Go to beginning of file (or _N percent into file).
t * Go to the (_N-th) next tag.
T * Go to the (_N-th) previous tag.
{ ( [ * Find close bracket } ) ].
} ) ] * Find open bracket { ( [.
ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>.
ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>.
---------------------------------------------------
Each "find close bracket" command goes forward to the close bracket
matching the (_N-th) open bracket in the top line.
Each "find open bracket" command goes backward to the open bracket
matching the (_N-th) close bracket in the bottom line.
m_<_l_e_t_t_e_r_> Mark the current top line with <letter>.
M_<_l_e_t_t_e_r_> Mark the current bottom line with <letter>.
'_<_l_e_t_t_e_r_> Go to a previously marked position.
'' Go to the previous position.
^X^X Same as '.
ESC-M_<_l_e_t_t_e_r_> Clear a mark.
---------------------------------------------------
A mark is any upper-case or lower-case letter.
Certain marks are predefined:
^ means beginning of the file
$ means end of the file
---------------------------------------------------------------------------
CCHHAANNGGIINNGG FFIILLEESS
:e [_f_i_l_e] Examine a new file.
^X^V Same as :e.
:n * Examine the (_N-th) next file from the command line.
:p * Examine the (_N-th) previous file from the command line.
:x * Examine the first (or _N-th) file from the command line.
:d Delete the current file from the command line list.
= ^G :f Print current file name.
---------------------------------------------------------------------------
MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS
-_<_f_l_a_g_> Toggle a command line option [see OPTIONS below].
--_<_n_a_m_e_> Toggle a command line option, by name.
__<_f_l_a_g_> Display the setting of a command line option.
___<_n_a_m_e_> Display the setting of an option, by name.
+_c_m_d Execute the less cmd each time a new file is examined.
!_c_o_m_m_a_n_d Execute the shell command with $SHELL.
|XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command.
s _f_i_l_e Save input to a file.
v Edit the current file with $VISUAL or $EDITOR.
V Print version number of "less".
---------------------------------------------------------------------------
OOPPTTIIOONNSS
Most options may be changed either on the command line,
or from within less by using the - or -- command.
Options may be given in one of two forms: either a single
character preceded by a -, or a name preceded by --.
-? ........ --help
Display help (from command line).
-a ........ --search-skip-screen
Search skips current screen.
-A ........ --SEARCH-SKIP-SCREEN
Search starts just after target line.
-b [_N] .... --buffers=[_N]
Number of buffers.
-B ........ --auto-buffers
Don't automatically allocate buffers for pipes.
-c ........ --clear-screen
Repaint by clearing rather than scrolling.
-d ........ --dumb
Dumb terminal.
-D xx_c_o_l_o_r . --color=xx_c_o_l_o_r
Set screen colors.
-e -E .... --quit-at-eof --QUIT-AT-EOF
Quit at end of file.
-f ........ --force
Force open non-regular files.
-F ........ --quit-if-one-screen
Quit if entire file fits on first screen.
-g ........ --hilite-search
Highlight only last match for searches.
-G ........ --HILITE-SEARCH
Don't highlight any matches for searches.
-h [_N] .... --max-back-scroll=[_N]
Backward scroll limit.
-i ........ --ignore-case
Ignore case in searches that do not contain uppercase.
-I ........ --IGNORE-CASE
Ignore case in all searches.
-j [_N] .... --jump-target=[_N]
Screen position of target lines.
-J ........ --status-column
Display a status column at left edge of screen.
-k [_f_i_l_e] . --lesskey-file=[_f_i_l_e]
Use a lesskey file.
-K ........ --quit-on-intr
Exit less in response to ctrl-C.
-L ........ --no-lessopen
Ignore the LESSOPEN environment variable.
-m -M .... --long-prompt --LONG-PROMPT
Set prompt style.
-n -N .... --line-numbers --LINE-NUMBERS
Don't use line numbers.
-o [_f_i_l_e] . --log-file=[_f_i_l_e]
Copy to log file (standard input only).
-O [_f_i_l_e] . --LOG-FILE=[_f_i_l_e]
Copy to log file (unconditionally overwrite).
-p [_p_a_t_t_e_r_n] --pattern=[_p_a_t_t_e_r_n]
Start at pattern (from command line).
-P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t]
Define new prompt.
-q -Q .... --quiet --QUIET --silent --SILENT
Quiet the terminal bell.
-r -R .... --raw-control-chars --RAW-CONTROL-CHARS
Output "raw" control characters.
-s ........ --squeeze-blank-lines
Squeeze multiple blank lines.
-S ........ --chop-long-lines
Chop (truncate) long lines rather than wrapping.
-t [_t_a_g] .. --tag=[_t_a_g]
Find a tag.
-T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e]
Use an alternate tags file.
-u -U .... --underline-special --UNDERLINE-SPECIAL
Change handling of backspaces.
-V ........ --version
Display the version number of "less".
-w ........ --hilite-unread
Highlight first new line after forward-screen.
-W ........ --HILITE-UNREAD
Highlight first new line after any forward movement.
-x [_N[,...]] --tabs=[_N[,...]]
Set tab stops.
-X ........ --no-init
Don't use termcap init/deinit strings.
-y [_N] .... --max-forw-scroll=[_N]
Forward scroll limit.
-z [_N] .... --window=[_N]
Set size of window.
-" [_c[_c]] . --quotes=[_c[_c]]
Set shell quote characters.
-~ ........ --tilde
Don't display tildes after end of file.
-# [_N] .... --shift=[_N]
Set horizontal scroll amount (0 = one half screen width).
--file-size
Automatically determine the size of the input file.
--follow-name
The F command changes files if the input file is renamed.
--incsearch
Search file as each pattern character is typed in.
--line-num-width=N
Set the width of the -N line number field to N characters.
--mouse
Enable mouse input.
--no-keypad
Don't send termcap keypad init/deinit strings.
--no-histdups
Remove duplicates from command history.
--rscroll=C
Set the character used to mark truncated lines.
--save-marks
Retain marks across invocations of less.
--status-col-width=N
Set the width of the -J status column to N characters.
--use-backslash
Subsequent options use backslash as escape char.
--use-color
Enables colored text.
--wheel-lines=N
Each click of the mouse wheel moves N lines.
---------------------------------------------------------------------------
LLIINNEE EEDDIITTIINNGG
These keys can be used to edit text being entered
on the "command line" at the bottom of the screen.
RightArrow ..................... ESC-l ... Move cursor right one character.
LeftArrow ...................... ESC-h ... Move cursor left one character.
ctrl-RightArrow ESC-RightArrow ESC-w ... Move cursor right one word.
ctrl-LeftArrow ESC-LeftArrow ESC-b ... Move cursor left one word.
HOME ........................... ESC-0 ... Move cursor to start of line.
END ............................ ESC-$ ... Move cursor to end of line.
BACKSPACE ................................ Delete char to left of cursor.
DELETE ......................... ESC-x ... Delete char under cursor.
ctrl-BACKSPACE ESC-BACKSPACE ........... Delete word to left of cursor.
ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor.
ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line.
UpArrow ........................ ESC-k ... Retrieve previous command line.
DownArrow ...................... ESC-j ... Retrieve next command line.
TAB ...................................... Complete filename & cycle.
SHIFT-TAB ...................... ESC-TAB Complete filename & reverse cycle.
ctrl-L ................................... Complete filename, list all.

136
ycopg2 Normal file
View File

@@ -0,0 +1,136 @@
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
Commands marked with * may be preceded by a number, _N.
Notes in parentheses indicate the behavior if _N is given.
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
h H Display this help.
q :q Q :Q ZZ Exit.
---------------------------------------------------------------------------
MMOOVVIINNGG
e ^E j ^N CR * Forward one line (or _N lines).
y ^Y k ^K ^P * Backward one line (or _N lines).
f ^F ^V SPACE * Forward one window (or _N lines).
b ^B ESC-v * Backward one window (or _N lines).
z * Forward one window (and set window to _N).
w * Backward one window (and set window to _N).
ESC-SPACE * Forward one window, but don't stop at end-of-file.
d ^D * Forward one half-window (and set half-window to _N).
u ^U * Backward one half-window (and set half-window to _N).
ESC-) RightArrow * Right one half screen width (or _N positions).
ESC-( LeftArrow * Left one half screen width (or _N positions).
ESC-} ^RightArrow Right to last column displayed.
ESC-{ ^LeftArrow Left to first column.
F Forward forever; like "tail -f".
ESC-F Like F but stop when search pattern is found.
r ^R ^L Repaint screen.
R Repaint screen, discarding buffered input.
---------------------------------------------------
Default "window" is the screen height.
Default "half-window" is half of the screen height.
---------------------------------------------------------------------------
SSEEAARRCCHHIINNGG
/_p_a_t_t_e_r_n * Search forward for (_N-th) matching line.
?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line.
n * Repeat previous search (for _N-th occurrence).
N * Repeat previous search in reverse direction.
ESC-n * Repeat previous search, spanning files.
ESC-N * Repeat previous search, reverse dir. & spanning files.
ESC-u Undo (toggle) search highlighting.
ESC-U Clear search highlighting.
&_p_a_t_t_e_r_n * Display only matching lines.
---------------------------------------------------
A search pattern may begin with one or more of:
^N or ! Search for NON-matching lines.
^E or * Search multiple files (pass thru END OF FILE).
^F or @ Start search at FIRST file (for /) or last file (for ?).
^K Highlight matches, but don't move (KEEP position).
^R Don't use REGULAR EXPRESSIONS.
^W WRAP search if no match found.
---------------------------------------------------------------------------
JJUUMMPPIINNGG
g < ESC-< * Go to first line in file (or line _N).
G > ESC-> * Go to last line in file (or line _N).
p % * Go to beginning of file (or _N percent into file).
t * Go to the (_N-th) next tag.
T * Go to the (_N-th) previous tag.
{ ( [ * Find close bracket } ) ].
} ) ] * Find open bracket { ( [.
ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>.
ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>.
---------------------------------------------------
Each "find close bracket" command goes forward to the close bracket
matching the (_N-th) open bracket in the top line.
Each "find open bracket" command goes backward to the open bracket
matching the (_N-th) close bracket in the bottom line.
m_<_l_e_t_t_e_r_> Mark the current top line with <letter>.
M_<_l_e_t_t_e_r_> Mark the current bottom line with <letter>.
'_<_l_e_t_t_e_r_> Go to a previously marked position.
'' Go to the previous position.
^X^X Same as '.
ESC-M_<_l_e_t_t_e_r_> Clear a mark.
---------------------------------------------------
A mark is any upper-case or lower-case letter.
Certain marks are predefined:
^ means beginning of the file
$ means end of the file
---------------------------------------------------------------------------
CCHHAANNGGIINNGG FFIILLEESS
:e [_f_i_l_e] Examine a new file.
^X^V Same as :e.
:n * Examine the (_N-th) next file from the command line.
:p * Examine the (_N-th) previous file from the command line.
:x * Examine the first (or _N-th) file from the command line.
:d Delete the current file from the command line list.
= ^G :f Print current file name.
---------------------------------------------------------------------------
MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS
-_<_f_l_a_g_> Toggle a command line option [see OPTIONS below].
--_<_n_a_m_e_> Toggle a command line option, by name.
__<_f_l_a_g_> Display the setting of a command line option.
___<_n_a_m_e_> Display the setting of an option, by name.
+_c_m_d Execute the less cmd each time a new file is examined.
!_c_o_m_m_a_n_d Execute the shell command with $SHELL.
|XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command.
s _f_i_l_e Save input to a file.
v Edit the current file with $VISUAL or $EDITOR.
V Print version number of "less".
---------------------------------------------------------------------------
OOPPTTIIOONNSS
Most options may be changed either on the command line,
or from within less by using the - or -- command.
Options may be given in one of two forms: either a single
character preceded by a -, or a name preceded by --.
-? ........ --help
Display help (from command line).
-a ........ --search-skip-screen
Search skips current screen.
-A ........ --SEARCH-SKIP-SCREEN
Search starts just after target line.
-b [_N] .... --buffers=[_N]
Number of buffers.
-B ........ --auto-buffers
Don't automatically allocate buffers for pipes.
-c ........ --clear-screen
Repaint by clearing rather than scrolling.
-d ........ --dumb
Dumb terminal.
-D xx_c_o_l_o_r . --color=xx_c_o_l_o_r
Set screen colors.
-e -E .... --quit-at-eof --QUIT-AT-EOF