Stacks Image 4000

Xanadu

Xanadu is a framework for developing Database Driven Web Apps.

Xanadu uses PHP, MySQL, HTML, Bootstrap, CSS, and Javascript with several amazing includes.

We've been developing Database Apps for decades with FileMaker and Xojo. FileMaker rocks but licensing became too expensive. Xojo has fantastic pricing, but is perpetually buggy. We had to find an alternative platform. PHP kept coming up in our searches. Long story short, we decided PHP is the best option just to get back to an affordable and solid platform.


Interested?

Xanadu is no longer publicly available, but private access is possible.
We offer Support and Training at $150/hour. We can:
  • Answer questions in detail via Email along with Zoom screen sharing.
  • Add features to Xanadu for you.
  • Assist you in adding features to Xanadu.
  • Develop a custom app for you using Xanadu as its foundation.
Xanadu is being used by several small businesses in different ways:
  • API Endpoint.
  • Customer Portal.
  • Web Store with a Shopping Cart.
  • Using FileMaker as a data source using the FileMaker Data API.
  • Production Job Scheduler.

Features

Modules
  • Modules
    • Home
    • Contacts
      • Actions: New, Print PDF, HTML, Duplicate, Delete
      • Contacts Comms ( Address, Email, Phone, Website, etc )
      • Actions: New, Delete, Address Map, Email Compose, Phone Call or Text, Web View
  • Login / Registration
    • Two Factor Authenticiation with Magic Links
    • Simple Password Rating: Weak, Moderate, Strong
    • Auto Logout
  • User Menu
    • Change My Password
    • Logout
    • Admin
      • Stats
        • Versions for OS, PHP, wkHTMLtoPDF
        • Disk
        • RAM
        • Processes,
        • Sessions
      • Settings
      • Users
        • Actions: Print PDF, HTML, Duplicate, Delete
        • Password Replace
      • API Requests
  • Pages
    • Bookmarkable for Quick Access
    • NavBar Activity Messages
UI Elements
  • Elements
    • Navbar
    • Buttons: Standard, Dropdown
    • Cards
      • List with Image
      • List as Table
      • Custom for Details
    • Card SearchBar
      • Simple Search
      • QueryBuilder
      • Sort Selector
    • Grid, GridRow
    • Table, StackTable, TableRow, TableCell
    • Tabs
    • NavItemModule, NavDropdownItemModuleItem, NavDivider
    • Modal, ModalButton
    • ImageURL
    • FontAwesome Icon
  • Forms
    • Database Inputs ( With Auto Massage to and from DB )
      • InputText, InputShowHide, InputHidden, InputTextArea,
      • InputDate, InputDateTime, InputTime, and Selectors
      • Autocomplete, Select, ValueListSQL, ValueListArray, ValueDoubler, OtherModal, Clear
      • FileUploadToBucketMeta
    • Formless Inputs
      • InputText, InputPassword, InputShowHide
Dev Tools
  • Server
    • Linux, Let's Encrypt, NGINX, PHP
  • Libraries / Includes
    • Autocomplete
    • Barcode
    • Bootstrap
    • Boostrap Icons
    • Flatpickr
    • fmREST
    • FontAwesome
    • Google Fonts
    • jQuery
    • LazyLoad for Images
    • Moment
    • PHPMailer for SMTP Email
    • QueryBuilder
    • StackTable
    • Street Standardize
    • Stripe for Payments
    • wkHTMLtoPDF for PDF Creation
  • API
    • Responses for Requests can be sent Immediately or Queued
    • Example for a 'RandomAmount'
  • Helpers
    • Dark and Light Modes
    • XSS Protection via Headers and Meta Tags
    • Secure Cookies
    • MailGun for Sending Emails
    • Twillo for Sending SMs / Text Message
    • XanDo for easy AJAX calls.
    • FontIcon function for easy use of Boostrap Icons and FontAwesome
    • SQL Functions
      • InsertOrUpdate
      • InsertQuestions
    • Social Media Meta Tags for Twitter and Open Graph

Overview


Home

Home is the place to include important information to the user. This might be a list of Tasks, Calendar Events, Billable Project Items, or Sales Stats.

Currently we have a few demos:
  • Getting a Location
  • Stripe Payment Buttons
  • QRCode
  • API Request and Queue Processor
  • Time from the Server in UTC and a equivalent time in a specified Timezone.
Stacks Image 4079

Contacts

Contacts is a Record Based Module that displays a record along with related records like Comms. Modules like Home are similar, except they don't typically have a Selected Record or a List.

The Menus are found on the top. Next is the Module Title with Module Action Buttons on the right. Buttons can be anywhere you'd like as well.

The Content is below the first two lines. In the Content, there is a List of Records Card with five Cards for the Details [ Contact, Comms, Info, Notes, and Other ].

Blue Plus Buttons can be found in the List to add a new Contact and also in Comms to add a new Address, Email, Web, Phone, etc. Red Minus Buttons can be found in Comms to delete a Comm.

Next to the Photo in the Contacts Card, a image can be uploaded by clicking the Blue Upload Button or Dragging and Dropping an image onto the Blue Upload Button.

The Contact Picker Button lets you search Contacts to then select a contact. It returns the Contact ID to then do something with that ID.

The Actions Button has a dropdown menu with items: Print to PDF, Print to HTML, Duplicate, and Delete.
Actions

Some modules have actions. Here we Add a Contact by clicking the + Button. On the right we can Select a Contact or under Actions, we can Print to PDF, Print to HMTL, Duplicate or Delete the selected Contact.
Stacks Image 5078

User Menu

The user Menu is for specific options for the logged in user.

The pictures show an Admin logged in. If a non Admin was logged the first four options would not be included.
Stacks Image 4960
Change Password

Users can change their password from the User Menu. As the New Password is entered, a simple password rating is displayed.
Stacks Image 5248

Stats

Stats is where you can see a bit into what is going on in the server. Stats shows:
  • Versions of the OS, PHP, and wkHTMLtoPDF
  • Disk Usage
  • RAM Usage
  • Disk Usage of each app folder
  • Processes Info
  • List of Sessions
  • List of Process Pools
Stacks Image 4978

Settings

Settings is where you can customize Xanadu to include:
  • Contact info
  • Specific formatting for Dates, Timestamps, Time, and Currency
  • Details for Sending Email ( SMTP )
  • Details for Sending SMS/Text Messages ( Twillo )
  • Details for Payment Buttons ( Stripe )
  • Other APIs like Google Maps
Stacks Image 5231

Users

To create a Login, a User is needed where you can set Contact Info, Privileges, and Authentication including replacing a password.
Stacks Image 5241

Activity Messages

We always want the user to know what is going on. Here we edit a User Full Name which immediately Saves followed by a Name Update in the Header and User List. The Name Update shows a Cloud icon indicating it started, an intentional pause for a few seconds, followed by the completion message and run time.
Stacks Image 5259

Development Flow

Our Development Flow is simple. Develop, Push to Production, and Push to GIT.

We use PHPStorm and love it. In PHP Storm your can connect to FTP Servers, GIT Servers, and MySQL. I'm sure there are many more things that you can connect to, but this is all we've needed. We create two domains each with their own website for Development and Production. Both are are added to PHPStorm as separate connections. Then we connect GIT to github.com and connect MySQL to the MySQL Server. Here's our flow:

  • Develop
    • Edit Code
    • Save Locally with Auto Upload to Development Site
    • Test Development Site on a browser.
    • Repeat alot :)
  • Production Push METHOD 1
    • Show GIT Changed Files in PHPStorm
    • Select Changed Files then Right Click the Files > Deployment > Upload to Production Site
    • Test Production Server on a Browser.
  • Production Push METHOD 2
    • Select Local Files and Folders to Check
    • Right Click choose Deployment > Sync with Deployed to.. Production Side
  • GIT Push
    • Choose Commit in PHPStorm GIT Menu
    • Choose Push in PHPStorm GIT Menu
    • On github.com, Pull the Changes into Master.

Tip: Use a hotkey for testing! Our hotkey switches to the browser and types command-option-R which reloads the page while reloading cached files.

File Routes

There are five main types of flows: Routed Page, Routed Do, Files, File Upload, and Scheduled Processes.

Routed Pages look like a normal link. When domain.com/contacts/123 is clicked:
  • NGINX intercepts the Request and sends it to index.php.
  • index.php loads init.php which loads Xanadu constants, classes, functions, and settings.
  • index.php loads Aloe for PHP from Tim Dietrich. Aloe helps with Sessions Management, Request Handling, and Response Handling.
  • index.php loads router.php.
  • router.php examines the path components, the 'contacts' and '123' parts, of the link and directs the processing of content-0-page.php for Contacts.
  • content-0-page.php directs the processing of content-1-cards which loads each specific card.
  • Finally return reaches back to index.php which returns the Aloe Response.

Routed Do are Xanadu's way of calling functions without reloading the Page via AJAX. AJAX leverages Javascript to make a Request and wait for a Response all in the background. The flow is similar to a Routed Page except data or a portion of the page are returned rather than an entire page. One example is Printing. When someone clicks the Actions > Print PDF:
  • XanDo function is called with parameters which immediately displays 'Printing PDF',
  • XanDo runs in the background and is routed like the Routed Page except router.php routes to contacts-do.php which is a sub router specifically for processing Contacts functions.
  • contacts-do.php examines the XanDo parameters and when it sees 'ContactsPrint', it processes do-contacts-print.php.
  • do-contacts-print.php queries for the Contact Record, creates the html, and creates a PDF from the html which is saved into a temp folder and a link to the PDF is returned.
  • XanDo sees that a URL was returned, opens a new tab/page and loads the URL to the PDF for the user.

File Downloads are accessed directly via a URL in either the 'bucket' or 'brief' directories. Files in Bucket would be like a Contact Photo. Files in Brief would have been created like when printing to PDF.

File Uploads are accessed directly via a URL. When a file is uploaded, it is moved to the Bucket specified sub directory like: domain.com/bucket/Contacts/123/PhotoFN/image.png and the File Name is stored on the Contact Record in the PhotoFN column.

Scheduled Processes are accessed directly via a URL to the php file. Files can be scheduled via crontab using curl to request the URL to the php file.

One Codebase with Many Workgroups

One of our goals with Xanadu is to have the ability to create Web Apps for Workgroups without having to duplicate the code for the entire site. We accomplish this with a Let's Encrypt Wildcard SSL Certificate like *.xanweb.app. Once the certificate is created any prefix will work like XANADU.xanweb.app or MARCOPOLO.xanweb.app as show below. Even HAL.xanweb.app could work, all with the same SSL Certificate. As indicated below, each Workgroup requires an init file, a database, and a files folder. Init files contain the credentials for the database and a few other private settings. Since there isn't an init for HAL.xanweb.app, a 404 Error Code would be returned.
Stacks Image 4823

Requirements

Xanadu requires PHP 7.4.x, Javascript, and MySQL.

Almost any hosting company should work, but we like Amazon Lightsail Linux Virtual Servers [ Nginx blueprint ], Managed Databases [ MySQL ], and using Let's Encrypt for SSL Security Certificates. To create PDFs, we use wkHTMLtoPDF.

Xanadu includes several libraries. Most are available without a fee. Some require a fee. If you are not interested in the fee based libraries, you can add your own alternatives.
- Icons uses FontAwesome Pro [ https://fontawesome.com/plans ]. We include the free version with less icons.
- Sending Emails requires MailGun [ https://www.mailgun.com/pricing/ ]
- Sending SMS / Text Messages requires Twillo [ https://www.twilio.com/pricing ]

If Javascript is disabled, the screen will be covered with a red background with the following text: "Xanadu cannot load without JavaScript. Here's how to enable JavaScript: Google Search or https://www.enable-javascript.com.

Lightsail

First, connect to your server via Lightsail's web based terminal found in your Lightsail Instance Settings and download your Lightsail SSH key. The SSH Key is needed to connect FTP and Terminal desktop apps via SFTP. We develop on Mac OS and use the Mac OS Terminal, Transmit for SFTP, "Navicat Essentials for MySQL" for the database. We use PHPStorm for our IDE which also connects via SFTP.

NGINX

Nginx is located at "/opt/bitnami/nginx/". Our friend Tim Dietrich suggests adding a sub directory "sites" along with a sub directory for each site. The site "app.foo.com", would be located at "/opt/bitnami/nginx/sites/app.foo.com/".

Nginx settings are located at "/opt/bitnami/nginx/conf/". Tim Dietrich suggests a structure conf with an "ningx.conf", an included "nginx_server.default.conf" and an included file for each server like "nginx_server.app.foo.com".

Creating the sites folders and Nginx conf files in this way makes it easy to add or move servers:

  • nginx.conf which has a ton of options. Read the comments and google as needed. Servers are 'included' at the bottom of the conf file.
  • nginx_server.default.conf is only exists to reject calls to domains and IP that are not specified as a server.
  • nginx_server.app.foo.com is an example of a server.
user daemon daemon;

# you must set worker processes based on your CPU cores, nginx does not benefit from setting more than that
worker_processes auto;

# number of file descriptors used for nginx
# the limit for the maximum FDs on the server is usually set by the OS.
# if you don't set FD's then OS settings will be used which is by default 2000
worker_rlimit_nofile 20000;

error_log "/opt/bitnami/nginx/logs/error.log";
pid "/opt/bitnami/nginx/logs/nginx.pid";

# provides the configuration file context in which the directives that affect connection processing are specified.
events {
# optimized to serve many clients with each thread, essential for linux
use epoll;

# determines how many clients will be served by each worker process
# max clients = worker_connections * worker_processes
# max clients is also limited by the number of socket connections available on the system (65535)
# Should be equal to `ulimit -n`
worker_connections 1024;

# accept as many connections as possible, may flood worker connections if set too low
multi_accept on;
}

http {

# mime
include mime.types;
default_type application/octet-stream;

# paths
client_body_temp_path "/opt/bitnami/nginx/tmp/client_body" 1 2;
proxy_temp_path "/opt/bitnami/nginx/tmp/proxy" 1 2;
fastcgi_temp_path "/opt/bitnami/nginx/tmp/fastcgi" 1 2;
scgi_temp_path "/opt/bitnami/nginx/tmp/scgi" 1 2;
uwsgi_temp_path "/opt/bitnami/nginx/tmp/uwsgi" 1 2;
access_log "/opt/bitnami/nginx/logs/access.log";

# gzip 4 is best option
gzip on;
gzip_http_version 1.1;
gzip_comp_level 4;
gzip_min_length 512;
gzip_proxied any;
gzip_vary on;
gzip_buffers 16 8k;
gzip_types text/plain
text/xml
text/css
text/javascript
application/json
application/javascript
application/x-javascript
application/ecmascript
application/xml
application/rss+xml
application/atom+xml
application/rdf+xml
application/xml+rss
application/xhtml+xml
application/x-font-ttf
application/x-font-opentype
application/vnd.ms-fontobject
image/svg+xml
image/x-icon
application/atom_xml;

# ssl
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS;

# fastcgi_param
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param HTTP_PROXY "";
fastcgi_param HTTPS $https if_not_empty;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REDIRECT_STATUS 200;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param REQUEST_SCHEME $scheme;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param SCRIPT_FILENAME $request_filename;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_NAME $server_name;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;

# Caches information about open FDs, freqently accessed files.
open_file_cache max=200000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;

# copies data between one FD and other from within the kernel ; faster than read() + write()
sendfile on;

# The maximum allowed size for a client request. If the maximum size is exceeded,
# then Nginx will spit out a 413 error or Request Entity Too Large.
client_max_body_size 100M;

# handles the client buffer size, meaning any POST actions sent to Nginx.
# POST actions are typically form submissions.
client_body_buffer_size 16K;

# handles the client header size. For all intents and purposes, 1K is usually a decent size for this directive
client_header_buffer_size 1k;

# Responsible for the time a server will wait for a client body or client header to be sent after request.
# If neither a body or header is sent, the server will issue a 408 error or Request time out. Default 60.
client_body_timeout 30;
client_header_timeout 30;

# M$IE closes keepalive connections in 60secs anyway.
# 90sec is a conservative upper bound.
# This number should ideally be the MAX number of
# seconds everything is donwlaoded.
# So if "time to interactive" on a web app is 20secs,
# Choosing 40secs (to be conservative) is a good ballpark
# guesstimate.
keepalive_timeout 90;

# if client stop responding, free up memory -- default 60
# established not on the entire transfer of answer, but only between two operations of reading;
# if after this time client will take nothing, then Nginx is shutting down the connection.
send_timeout 60;

# allow the server to close connection on non responding client, this will free up memory
reset_timedout_connection on;

# to boost I/O on HDD we can disable access logs
# access_log off;

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# Servers

# Default server
include nginx_server.default.conf;

# Xanadu
include nginx_server.app.foo.com;
}
# --------------------------------------------------------------------------
# Default Host
# --------------------------------------------------------------------------

# Disables the default host.
# See: http://serverfault.com/questions/420351/best-way-to-prevent-default-server
server {
listen 80 default_server;
server_name _;
add_header Permissions-Policy interest-cohort=();
return 444;
# return 301 https://campsoftware.com;
}
server {
server_name app.foo.com;
listen 80;
location '/.well-known/acme-challenge' {
add_header Permissions-Policy interest-cohort=();
default_type "text/plain";
root /opt/bitnami/nginx/sites/app.foo.com;
}
location / {
add_header Permissions-Policy interest-cohort=();
return 301 https://$server_name$request_uri;
}
}

# COMMENT OUT the following until the cert is created.
server {
# Server name and port.
server_name app.foo.com;
listen 443 ssl;

# SSL certificate location.
ssl_certificate /etc/letsencrypt/live/app.foo.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.foo.com/privkey.pem;

# Log file names / locations.
access_log /opt/bitnami/nginx/logs/app.foo.com-access.log;
error_log /opt/bitnami/nginx/logs/app.foo.com-error.log;

# Web app root location.
root sites/app.foo.com/;

# Web app index files.
index index.php;

# Remove the nginx version from the Server response header.
server_tokens off;

# 404 "not found" handler.
# error_page 404 = /*index.php;
# The problem with using "error_page" for dynamic routing is that
# the request will always appear to PHP as an HTTP GET request.

# css / js / media
location ~* \.(?:css|js|jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
add_header Permissions-Policy interest-cohort=();
add_header X-Frame-Options SAMEORIGIN;
add_header Cache-Control "public";
expires 1d;
access_log off;
}

# If the requested resource isn't available,
# then pass the request to the app's core script.
location / {
add_header Permissions-Policy interest-cohort=();
add_header X-Frame-Options SAMEORIGIN;
try_files $uri $uri/ /index.php?$query_string;
}

# PHP processing...
location ~ \.php$ {
add_header Permissions-Policy interest-cohort=();
add_header X-Frame-Options SAMEORIGIN;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_read_timeout 300;
fastcgi_pass unix:/opt/bitnami/php/var/run/www.sock;
}
}

Let's Encrypt

Install
sudo apt update
sudo apt install snapd
sudo snap install core
sudo snap install hello-world
hello-world
sudo snap install core; sudo snap refresh core
sudo apt-get remove certbot
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
sudo certbot renew --dry-run

Setup
  • Add new DNS A record for the Server IP.
  • Add the Certificate. We like create wildcard certificates:
    • sudo certbot -d xanweb.app -d *.xanweb.app --manual --preferred-challenges dns certonly
    • During the creation process you'll need to Add DNS TXT records to prove ownership.

PDF Engine

wkHTMLtoPDF

We use wkHTMLtoPDF to create PDFs from HTML. To install run this commands:
  • "sudo apt-get update" to update what you can install.
  • "apt-cache policy wkhtmltopdf" to see what version is available.
  • "sudo apt-get install wkhtmltopdf" to install.

Tips

Use Hotkeys for Repetitive Tasks

Use a hotkey for testing! Our hotkey switches to the browser and types command-option-R which reloads the page while reloading cached files.
Naming Conventions

  • Database Columns
    • Primary and Foreign Keys: Begin with "UUID" followed by the Table Name.
    • Mod Fields: Begin with "Mod"
    • Timestamps: End in "TS"
    • Booleans: Instead use VarChar(3) for "Yes" or "No". This makes Interface and Reporting simple. Prefixed with "Is" like IsSent
    • Filenames: End in "FN"
  • Variables
    • Arrays end with "A"
    • Dictionaries / Associative Arrays end with "D"
    • Booleans start with "Is" and/or end with "B"
    • Module Classes begin with "mm". Table Classes also end with "T" like $mmContactsT
Quoting Code

Quoting Code can be complex because when you use PHP to write out HTML with Javascript. You can end up with a sting within a string within a string.

We try to use:
  • PHP: Single Quotes
  • PHP: Double Quotes if relying on PHP to evaluate like "Hello $firstname"
  • HTML: Double Quotes
  • Javascript: Back Tick
  • Mixed Quotes: PHP defining an HTML attribute with a Javascript function
    • Ex: 'onclick="xanLocationGet( function ( coords ) { alert( coords[`ErrorCode`] + `, ` + coords[`ErrorDesc`] + `, ` + coords[`Latitude`] + `, ` + coords[`Longitude`] + `, ` + coords[`Altitude`] ); } );"'
Fav Icons

Starting with a square icon image around 512px by 512px, generate icons with a web app like Iconifier https://iconifier.net which generates each image and the required HTML.
Barcodes

Barcodes are easy to generate. All you need is to create a URL like: "barcode.php?f=png&s=qr&p=-14&d=http://campsoftware.com" to generate the barcode on the right.

A few options:
  • f = Formats: png, gif, jpeg, svg
  • f = Symbology: codabar, code-128, code-39, code-39-ascii, code-93, code-93-ascii, dmtx, dmtx-r, dmtx-s, ean-128, ean-13, ean-13-nopad, ean-13-pad, ean-8, gs1-dmtx, gs1-dmtx-r, gs1-dmtx-s, itf, qr, qr-h, qr-l, qr-m, qr-q, upc-a, upc-e
  • p = Padding
  • d = Data

More info: https://github.com/kreativekorp/barcode
Stacks Image 4904

Questions and Answers

Q: Can you add a feature for me?
A: That depends on what you need! :) Seriously, just ask and we'll try!
Q: Why don't you use FileMaker?
A: We did use FileMaker for years! Unfortunately, the licensing made selling apps with FileMaker too expensive. We used FileMaker Runtime, but it was deprecated. We hoped to use FileMaker WebDirect, but FileMaker changed the WebDirect per user price to the same price as FileMaker Pro or FileMaker Go user. FileMaker just priced it self out. :(
Q: Why don't you use Xojo aka REAL Studio aka REALbasic?
A: We tried Xojo. While developing Desktop Apps [ Mac, Windows, Linux ] works well, not so much for Web Apps. When developing Xanadu with Web 1.0 would encounter a Xojo bug here and there, the IDE wouldn't crash too much, but over time the quality decreased. Xojo deprecated Web 1.0 with the release of Web 2.0. We were excited for responsive apps and a refresh. Unfortunately it look Xojo a while and once released, Web 2.0 sure didn't feel ready. I love Xojo and the pricing model. I'd pay double or even triple, but there's just too many bugs in Xojo to deal with. Check out the forums at https://forum.xojo.com and https://ifnotnil.com to read more.