Compare commits
70
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
587b321205 | ||
|
|
fe06477bec | ||
|
|
ffaea83e09 | ||
|
|
52e85b806c | ||
|
|
a78a2df86f | ||
|
|
0b0be06a4d | ||
|
|
d10f4ceb69 | ||
|
|
805a60db72 | ||
|
|
ab18085b8c | ||
|
|
1e4a188acb | ||
|
|
61b3f2da7b | ||
|
|
09bb22ea1a | ||
|
|
da013ff6ce | ||
|
|
edf9665502 | ||
|
|
8b2c19b7c6 | ||
|
|
cf4827c09c | ||
|
|
c424f0b32a | ||
|
|
33c27c1cba | ||
|
|
ac5c83506d | ||
|
|
0f3b0ccafa | ||
|
|
7e3bd6f665 | ||
|
|
4e860d14e6 | ||
|
|
3b96c59bcf | ||
|
|
d933723fce | ||
|
|
b7f9afb3f0 | ||
|
|
9b25a0b93a | ||
|
|
c9861ddbfc | ||
|
|
4a386dd4dc | ||
|
|
c1e9013353 | ||
|
|
bd510b5e89 | ||
|
|
4c1fcc60ed | ||
|
|
9cf9067a64 | ||
|
|
9f34c2d544 | ||
|
|
624cf64c3b | ||
|
|
3fe4659377 | ||
|
|
12b14da970 | ||
|
|
0271538b80 | ||
|
|
2696e635e2 | ||
|
|
17e3296774 | ||
|
|
fd07ddf976 | ||
|
|
cb01a25c7d | ||
|
|
5c5330d93f | ||
|
|
52eb6a9c85 | ||
|
|
72e1f9d9bf | ||
|
|
d347310dc4 | ||
|
|
d38bea0cc2 | ||
|
|
baabe90000 | ||
|
|
9d2f92060f | ||
|
|
49a291ba5a | ||
|
|
50de5ebd95 | ||
|
|
0f295956ca | ||
|
|
03f2ffb1f2 | ||
|
|
ea9e8825c3 | ||
|
|
643370f4f8 | ||
|
|
460c511de7 | ||
|
|
63a0fa1b22 | ||
|
|
1d42d1efa6 | ||
|
|
ed4bf51c1c | ||
|
|
f4999f4d19 | ||
|
|
1805d0531b | ||
|
|
ff3d5ef48d | ||
|
|
da1210d682 | ||
|
|
bd1488c78a | ||
|
|
89ef938af4 | ||
|
|
ca3825660b | ||
|
|
50501a3d37 | ||
|
|
613c906626 | ||
|
|
b91245c8a1 | ||
|
|
56e771c454 | ||
|
|
faa1c5437b |
Regular → Executable
+1
@@ -1,3 +1,4 @@
|
|||||||
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
||||||
*.jpg filter=lfs diff=lfs merge=lfs -text
|
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||||
*.png filter=lfs diff=lfs merge=lfs -text
|
*.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.svg filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
__pycache__
|
||||||
|
.vscode
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[FORMAT]
|
||||||
|
max-line-length=140
|
||||||
|
|
||||||
|
[MESSAGES CONTROL]
|
||||||
|
disable=import-error
|
||||||
Regular → Executable
+8
-5
@@ -1,8 +1,11 @@
|
|||||||
FROM httpd:2.4
|
FROM python:3.13-bookworm
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
RUN apt-get -y install libapache2-mod-wsgi-py3 python3 python3-pip
|
RUN apt-get -y install apache2 apache2-dev
|
||||||
COPY src/requirements.txt /var/www/jc/requirements.txt
|
COPY src/requirements.txt /var/www/jc/requirements.txt
|
||||||
RUN pip3 install -r /var/www/jc/requirements.txt || pip3 install --break-system-packages -r /var/www/jc/requirements.txt
|
RUN /usr/local/bin/pip3 install --upgrade pip
|
||||||
COPY --chown=www-data:www-data config/httpd.conf /usr/local/apache2/conf/httpd.conf
|
RUN /usr/local/bin/pip3 install -r /var/www/jc/requirements.txt
|
||||||
|
COPY --chown=www-data:www-data config/httpd.conf /etc/apache2/apache2.conf
|
||||||
COPY --chown=www-data:www-data src/ /var/www/jc
|
COPY --chown=www-data:www-data src/ /var/www/jc
|
||||||
RUN httpd -t
|
RUN apache2 -t
|
||||||
|
EXPOSE 80
|
||||||
|
ENTRYPOINT ["apache2", "-D", "FOREGROUND"]
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
|
||||||
|
environment {
|
||||||
|
TS = credentials('jc_turnstile')
|
||||||
|
DISCORD = credentials('jc_discord')
|
||||||
|
DISCORD_ERR_STAGING = credentials('jc_discord_err_staging')
|
||||||
|
DISCORD_ERR_PROD = credentials('jc_discord_err_prod')
|
||||||
|
}
|
||||||
|
|
||||||
|
stages{
|
||||||
|
stage ('Set up parameters') {
|
||||||
|
steps {
|
||||||
|
script{
|
||||||
|
properties ([
|
||||||
|
parameters([
|
||||||
|
booleanParam(defaultValue: true, description: 'Build from source', name: 'Build'),
|
||||||
|
booleanParam(defaultValue: true, description: 'Deploy to servers', name: 'Deploy'),
|
||||||
|
booleanParam(defaultValue: true, description: 'Update posts', name: 'Update'),
|
||||||
|
])
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Build') {
|
||||||
|
when {
|
||||||
|
expression {
|
||||||
|
return params.Build == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
git branch: 'master',
|
||||||
|
credentialsId: 'Git',
|
||||||
|
url: '[email protected]:jake/jc-ng.git'
|
||||||
|
|
||||||
|
sh "./build.sh git.jakecharman.co.uk/jake/jakecharman.co.uk $BUILD_NUMBER"
|
||||||
|
sh "./build.sh europe-west2-docker.pkg.dev/jakecharman/web/jakecharman.co.uk $BUILD_NUMBER"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Security scan') {
|
||||||
|
when {
|
||||||
|
expression {
|
||||||
|
return params.Build == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
sh "docker kill sectest || true"
|
||||||
|
sh "docker rm sectest || true"
|
||||||
|
sh "docker run -d --name sectest git.jakecharman.co.uk/jake/jakecharman.co.uk:$BUILD_NUMBER"
|
||||||
|
sh "docker exec sectest pip3 install pip-audit --break-system-packages"
|
||||||
|
sh "docker exec sectest pip-audit"
|
||||||
|
sh "docker stop sectest"
|
||||||
|
sh "docker rm sectest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Push to local registry') {
|
||||||
|
when {
|
||||||
|
expression {
|
||||||
|
return params.Build == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
sh "docker push git.jakecharman.co.uk/jake/jakecharman.co.uk:$BUILD_NUMBER"
|
||||||
|
sh "docker push git.jakecharman.co.uk/jake/jakecharman.co.uk:latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Deploy to staging server') {
|
||||||
|
when {
|
||||||
|
expression {
|
||||||
|
return params.Deploy == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps{
|
||||||
|
node('web-staging') {
|
||||||
|
sh "docker pull git.jakecharman.co.uk/jake/jakecharman.co.uk:latest"
|
||||||
|
sh "docker stop jake || true"
|
||||||
|
sh "docker rm jake || true"
|
||||||
|
sh "docker run --name jake -e DISCORD_ERR_HOOK=$DISCORD_ERR_STAGING -e DISCORD_WEBHOOK=$DISCORD -e TURNSTILE_SECRET=$TS --restart always --network containers_default -v /opt/containers/jc/projects/:/var/www/jc/projects/ -d git.jakecharman.co.uk/jake/jakecharman.co.uk:latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Update content on staging server') {
|
||||||
|
when {
|
||||||
|
expression {
|
||||||
|
return params.Update == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
node('web-staging') {
|
||||||
|
git branch: 'master',
|
||||||
|
credentialsId: 'Git',
|
||||||
|
url: '[email protected]:jake/jc-content.git'
|
||||||
|
sh "rsync -rv --delete ./ /opt/containers/jc/projects/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Wait for confirmation to push to prod') {
|
||||||
|
steps {
|
||||||
|
timeout(time: 30, unit: 'MINUTES') {
|
||||||
|
input "Deploy to production?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Push to GCP registry') {
|
||||||
|
when {
|
||||||
|
expression {
|
||||||
|
return params.Build == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
sh "docker push europe-west2-docker.pkg.dev/jakecharman/web/jakecharman.co.uk:latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Deploy to production') {
|
||||||
|
when {
|
||||||
|
expression {
|
||||||
|
return params.Deploy == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps{
|
||||||
|
sh "gcloud run deploy --project jakecharman --region europe-west1 --image europe-west2-docker.pkg.dev/jakecharman/web/jakecharman.co.uk:latest jakecharman-co-uk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Update content on production server') {
|
||||||
|
when {
|
||||||
|
expression {
|
||||||
|
return params.Update == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
git branch: 'master',
|
||||||
|
credentialsId: 'Git',
|
||||||
|
url: '[email protected]:jake/jc-content.git'
|
||||||
|
sh "gsutil rsync -rcd . gs://jakecharman.co.uk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Clear cache') {
|
||||||
|
steps{
|
||||||
|
sh "/var/lib/jenkins/clearCFCache/clearCache.py a514fb61e1413b88aabbb19df16b8508"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash --noprofile
|
||||||
|
|
||||||
|
set -x -o pipefail
|
||||||
|
|
||||||
|
tag=$1
|
||||||
|
build=$2
|
||||||
|
|
||||||
|
cat <<EOF >src/.buildinfo.json
|
||||||
|
{
|
||||||
|
"tag": "${tag}:${build}",
|
||||||
|
"date": "$(date -I)",
|
||||||
|
"host": "$(hostname -f)",
|
||||||
|
"user": "${USER}"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
docker build -t ${tag}:latest .
|
||||||
|
if [[ $build != "" ]]; then
|
||||||
|
docker build -t ${tag}:${build} .
|
||||||
|
fi
|
||||||
Regular → Executable
+14
-8
@@ -28,7 +28,7 @@
|
|||||||
# same ServerRoot for multiple httpd daemons, you will need to change at
|
# same ServerRoot for multiple httpd daemons, you will need to change at
|
||||||
# least PidFile.
|
# least PidFile.
|
||||||
#
|
#
|
||||||
ServerRoot "/usr/local/apache2"
|
ServerRoot "/usr/lib/apache2"
|
||||||
|
|
||||||
#
|
#
|
||||||
# Mutex: Allows you to set the mutex mechanism and mutex file directory
|
# Mutex: Allows you to set the mutex mechanism and mutex file directory
|
||||||
@@ -123,7 +123,7 @@ LoadModule deflate_module modules/mod_deflate.so
|
|||||||
#LoadModule brotli_module modules/mod_brotli.so
|
#LoadModule brotli_module modules/mod_brotli.so
|
||||||
LoadModule mime_module modules/mod_mime.so
|
LoadModule mime_module modules/mod_mime.so
|
||||||
#LoadModule ldap_module modules/mod_ldap.so
|
#LoadModule ldap_module modules/mod_ldap.so
|
||||||
LoadModule log_config_module modules/mod_log_config.so
|
#LoadModule log_config_module modules/mod_log_config.so
|
||||||
#LoadModule log_debug_module modules/mod_log_debug.so
|
#LoadModule log_debug_module modules/mod_log_debug.so
|
||||||
#LoadModule log_forensic_module modules/mod_log_forensic.so
|
#LoadModule log_forensic_module modules/mod_log_forensic.so
|
||||||
#LoadModule logio_module modules/mod_logio.so
|
#LoadModule logio_module modules/mod_logio.so
|
||||||
@@ -137,7 +137,7 @@ LoadModule headers_module modules/mod_headers.so
|
|||||||
#LoadModule usertrack_module modules/mod_usertrack.so
|
#LoadModule usertrack_module modules/mod_usertrack.so
|
||||||
#LoadModule unique_id_module modules/mod_unique_id.so
|
#LoadModule unique_id_module modules/mod_unique_id.so
|
||||||
LoadModule setenvif_module modules/mod_setenvif.so
|
LoadModule setenvif_module modules/mod_setenvif.so
|
||||||
LoadModule version_module modules/mod_version.so
|
#LoadModule version_module modules/mod_version.so
|
||||||
#LoadModule remoteip_module modules/mod_remoteip.so
|
#LoadModule remoteip_module modules/mod_remoteip.so
|
||||||
#LoadModule proxy_module modules/mod_proxy.so
|
#LoadModule proxy_module modules/mod_proxy.so
|
||||||
#LoadModule proxy_connect_module modules/mod_proxy_connect.so
|
#LoadModule proxy_connect_module modules/mod_proxy_connect.so
|
||||||
@@ -171,7 +171,7 @@ LoadModule version_module modules/mod_version.so
|
|||||||
#LoadModule lbmethod_bytraffic_module modules/mod_lbmethod_bytraffic.so
|
#LoadModule lbmethod_bytraffic_module modules/mod_lbmethod_bytraffic.so
|
||||||
#LoadModule lbmethod_bybusyness_module modules/mod_lbmethod_bybusyness.so
|
#LoadModule lbmethod_bybusyness_module modules/mod_lbmethod_bybusyness.so
|
||||||
#LoadModule lbmethod_heartbeat_module modules/mod_lbmethod_heartbeat.so
|
#LoadModule lbmethod_heartbeat_module modules/mod_lbmethod_heartbeat.so
|
||||||
LoadModule unixd_module modules/mod_unixd.so
|
#LoadModule unixd_module modules/mod_unixd.so
|
||||||
#LoadModule heartbeat_module modules/mod_heartbeat.so
|
#LoadModule heartbeat_module modules/mod_heartbeat.so
|
||||||
#LoadModule heartmonitor_module modules/mod_heartmonitor.so
|
#LoadModule heartmonitor_module modules/mod_heartmonitor.so
|
||||||
#LoadModule dav_module modules/mod_dav.so
|
#LoadModule dav_module modules/mod_dav.so
|
||||||
@@ -198,8 +198,7 @@ LoadModule dir_module modules/mod_dir.so
|
|||||||
LoadModule alias_module modules/mod_alias.so
|
LoadModule alias_module modules/mod_alias.so
|
||||||
#LoadModule rewrite_module modules/mod_rewrite.somod_wsgi-express start-server
|
#LoadModule rewrite_module modules/mod_rewrite.somod_wsgi-express start-server
|
||||||
|
|
||||||
LoadModule wsgi_module /usr/lib/apache2/modules/mod_wsgi.so
|
LoadModule wsgi_module /usr/local/lib/python3.13/site-packages/mod_wsgi/server/mod_wsgi-py313.cpython-313-x86_64-linux-gnu.so
|
||||||
|
|
||||||
|
|
||||||
<IfModule unixd_module>
|
<IfModule unixd_module>
|
||||||
#
|
#
|
||||||
@@ -368,7 +367,7 @@ LogLevel warn
|
|||||||
# TypesConfig points to the file containing the list of mappings from
|
# TypesConfig points to the file containing the list of mappings from
|
||||||
# filename extension to MIME-type.
|
# filename extension to MIME-type.
|
||||||
#
|
#
|
||||||
TypesConfig conf/mime.types
|
TypesConfig /etc/mime.types
|
||||||
|
|
||||||
#
|
#
|
||||||
# AddType allows you to add to or override the MIME configuration
|
# AddType allows you to add to or override the MIME configuration
|
||||||
@@ -423,7 +422,6 @@ ErrorDocument 503 /error/503
|
|||||||
ErrorDocument 505 /error/505
|
ErrorDocument 505 /error/505
|
||||||
|
|
||||||
WSGISocketPrefix /var/run/wsgi
|
WSGISocketPrefix /var/run/wsgi
|
||||||
|
|
||||||
WSGIDaemonProcess jc-wsgi user=www-data group=www-data threads=5
|
WSGIDaemonProcess jc-wsgi user=www-data group=www-data threads=5
|
||||||
WSGIProcessGroup jc-wsgi
|
WSGIProcessGroup jc-wsgi
|
||||||
WSGIScriptAlias / /var/www/jc/projects.wsgi
|
WSGIScriptAlias / /var/www/jc/projects.wsgi
|
||||||
@@ -434,6 +432,14 @@ WSGIScriptAlias / /var/www/jc/projects.wsgi
|
|||||||
Require all granted
|
Require all granted
|
||||||
</Directory>
|
</Directory>
|
||||||
|
|
||||||
|
Alias "/static" "/var/www/jc/static/"
|
||||||
|
<Directory /var/www/jc/static>
|
||||||
|
Order allow,Deny
|
||||||
|
Allow from all
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
Alias "/robots.txt" "/var/www/jc/static/robots.txt"
|
||||||
|
|
||||||
<Files jc.wsgi>
|
<Files jc.wsgi>
|
||||||
Require all granted
|
Require all granted
|
||||||
</Files>
|
</Files>
|
||||||
|
|||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash --noprofile
|
||||||
|
|
||||||
|
run() {
|
||||||
|
python3 src/projects.wsgi || run
|
||||||
|
}
|
||||||
|
|
||||||
|
export DISCORD_ERR_HOOK='dummy'
|
||||||
|
run
|
||||||
|
unset DISCORD_ERR_HOOK
|
||||||
Executable
+12
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash --noprofile
|
||||||
|
|
||||||
|
set -x -o pipefail
|
||||||
|
|
||||||
|
./build.sh "jc-ng-localtest"
|
||||||
|
|
||||||
|
content_dir=""
|
||||||
|
if [[ -d "../jc-content" ]]; then
|
||||||
|
content_dir="-v $(realpath ../jc-content):/var/www/jc/projects"
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run -e DISCORD_ERR_HOOK=dummy $1 -v $(pwd)/src/:/var/www/jc $content_dir jc-ng-localtest
|
||||||
Regular → Executable
+11
-8
@@ -6,7 +6,7 @@ from flask import request, render_template
|
|||||||
from requests import post, get
|
from requests import post, get
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
from traceback import format_exc
|
||||||
def validate_turnstile(response: str, ip: str) -> bool:
|
def validate_turnstile(response: str, ip: str) -> bool:
|
||||||
turnstile_secret = environ['TURNSTILE_SECRET']
|
turnstile_secret = environ['TURNSTILE_SECRET']
|
||||||
cf_response = post(
|
cf_response = post(
|
||||||
@@ -16,7 +16,8 @@ def validate_turnstile(response: str, ip: str) -> bool:
|
|||||||
'response': response,
|
'response': response,
|
||||||
'remoteip': ip,
|
'remoteip': ip,
|
||||||
'idempotency_key': uuid4()
|
'idempotency_key': uuid4()
|
||||||
}
|
},
|
||||||
|
timeout=30
|
||||||
).json()
|
).json()
|
||||||
|
|
||||||
return cf_response.get('success', False)
|
return cf_response.get('success', False)
|
||||||
@@ -24,7 +25,8 @@ def validate_turnstile(response: str, ip: str) -> bool:
|
|||||||
def send_to_discord(form: dict) -> bool:
|
def send_to_discord(form: dict) -> bool:
|
||||||
try:
|
try:
|
||||||
discord_hook = environ['DISCORD_WEBHOOK']
|
discord_hook = environ['DISCORD_WEBHOOK']
|
||||||
except:
|
except KeyError:
|
||||||
|
app.logger.error(format_exc())
|
||||||
return False
|
return False
|
||||||
discord_msg = dedent(
|
discord_msg = dedent(
|
||||||
f'''
|
f'''
|
||||||
@@ -42,11 +44,12 @@ def send_to_discord(form: dict) -> bool:
|
|||||||
data={
|
data={
|
||||||
'username': form.get('name'),
|
'username': form.get('name'),
|
||||||
'content': discord_msg
|
'content': discord_msg
|
||||||
}
|
},
|
||||||
).status_code
|
timeout=30
|
||||||
|
)
|
||||||
if discord_response == 204:
|
if discord_response.status_code == 204:
|
||||||
return True
|
return True
|
||||||
|
app.logger.error(discord_response.status_code, discord_response.text)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@app.route('/contact/', methods=('GET', 'POST'))
|
@app.route('/contact/', methods=('GET', 'POST'))
|
||||||
@@ -59,4 +62,4 @@ def contact():
|
|||||||
return render_template('contact.html', user_message='Your message has been sent!')
|
return render_template('contact.html', user_message='Your message has been sent!')
|
||||||
return render_template('contact.html', error=True, user_message='An error occurred.')
|
return render_template('contact.html', error=True, user_message='An error occurred.')
|
||||||
else:
|
else:
|
||||||
return render_template('contact.html')
|
return render_template('contact.html', page_title='Contact - ')
|
||||||
|
|||||||
Regular → Executable
+52
-5
@@ -1,18 +1,60 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
from os import environ
|
||||||
|
import threading
|
||||||
|
import logging
|
||||||
|
from requests import post
|
||||||
from flask import Flask, render_template, Response
|
from flask import Flask, render_template, Response
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
import projects
|
# These imports need to come after our app is defined as they add routes to it.
|
||||||
import contact
|
import projects # pylint: disable=wrong-import-position,unused-import
|
||||||
|
import contact # pylint: disable=wrong-import-position,unused-import
|
||||||
|
import sitemap # pylint: disable=wrong-import-position,unused-import
|
||||||
|
|
||||||
|
class DiscordLogger(logging.Handler):
|
||||||
|
''' Simple logging handler to send a message to Discord '''
|
||||||
|
|
||||||
|
level = logging.ERROR
|
||||||
|
|
||||||
|
def __init__(self, webhook):
|
||||||
|
super().__init__()
|
||||||
|
self._webhook = webhook
|
||||||
|
|
||||||
|
def send_to_discord(self, msg: logging.LogRecord):
|
||||||
|
''' Send the message '''
|
||||||
|
if msg.exc_info is not None:
|
||||||
|
message_to_send = f'{msg.msg}\n\n{"".join(traceback.format_exception(*msg.exc_info))}'
|
||||||
|
else:
|
||||||
|
message_to_send = msg.msg
|
||||||
|
if len(message_to_send) > 2000:
|
||||||
|
chars_to_lose = len(message_to_send) - 2000
|
||||||
|
if msg.exc_info is not None:
|
||||||
|
message_to_send = f'{msg.msg}\n\n{"".join(traceback.format_exception(*msg.exc_info))[-chars_to_lose:]}'
|
||||||
|
else:
|
||||||
|
message_to_send = msg.msg[-chars_to_lose:]
|
||||||
|
post(self._webhook, data={'content': message_to_send}, timeout=30)
|
||||||
|
|
||||||
|
def emit(self, record: logging.LogRecord) -> None:
|
||||||
|
''' Take in the record and start a new thread to send it to Discord '''
|
||||||
|
app.logger.info('Sending error to Discord')
|
||||||
|
dc_thread = threading.Thread(target=self.send_to_discord, args=[record])
|
||||||
|
dc_thread.start()
|
||||||
|
|
||||||
|
discord_logger = DiscordLogger(environ['DISCORD_ERR_HOOK'])
|
||||||
|
app.logger.addHandler(discord_logger)
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index() -> str:
|
||||||
|
''' Load the homepage '''
|
||||||
return render_template('index.html')
|
return render_template('index.html')
|
||||||
|
|
||||||
@app.route('/error/<code>')
|
@app.route('/error/<code>')
|
||||||
def error(code):
|
def error(code) -> str:
|
||||||
|
''' Render a nicer error page for a given code '''
|
||||||
|
|
||||||
error_definitions = {
|
error_definitions = {
|
||||||
400: 'Bad Request',
|
400: 'Bad Request',
|
||||||
403: 'Forbidden',
|
403: 'Forbidden',
|
||||||
@@ -32,6 +74,11 @@ def error(code):
|
|||||||
505: 'Your browser tried to use a HTTP version I don\'t support. Check it is up to date.'
|
505: 'Your browser tried to use a HTTP version I don\'t support. Check it is up to date.'
|
||||||
}
|
}
|
||||||
|
|
||||||
return render_template('error.html',
|
if not code.isdigit():
|
||||||
|
code=400
|
||||||
|
elif int(code) not in error_definitions:
|
||||||
|
return Response(status=code)
|
||||||
|
|
||||||
|
return render_template('error.html',
|
||||||
error=f'{code}: {error_definitions.get(int(code))}',
|
error=f'{code}: {error_definitions.get(int(code))}',
|
||||||
description=error_desc.get(int(code)))
|
description=error_desc.get(int(code)))
|
||||||
|
|||||||
Regular → Executable
+67
-25
@@ -2,19 +2,22 @@
|
|||||||
|
|
||||||
from os import path
|
from os import path
|
||||||
import json
|
import json
|
||||||
from flask import Flask, render_template, Response, send_from_directory
|
|
||||||
from markdown import markdown
|
|
||||||
import frontmatter
|
|
||||||
from glob import glob
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from index import app
|
from io import BytesIO
|
||||||
|
from glob import glob
|
||||||
|
from PIL import Image, UnidentifiedImageError
|
||||||
|
import frontmatter
|
||||||
|
from markdown import markdown
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
from flask import render_template, Response, send_from_directory, request, make_response
|
||||||
|
from index import app
|
||||||
|
|
||||||
md_directory = path.join(path.realpath(path.dirname(__file__)), path.normpath('projects/'))
|
md_directory = path.join(path.realpath(path.dirname(__file__)), path.normpath('projects/'))
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def processor():
|
def processor() -> dict:
|
||||||
def get_excerpt(post):
|
''' Jninja processors '''
|
||||||
|
def get_excerpt(post: frontmatter.Post) -> str:
|
||||||
html = markdown(post.content)
|
html = markdown(post.content)
|
||||||
post_soup = BeautifulSoup(html, 'html.parser')
|
post_soup = BeautifulSoup(html, 'html.parser')
|
||||||
all_text = ' '.join([x.get_text() for x in post_soup.findAll('p')])
|
all_text = ' '.join([x.get_text() for x in post_soup.findAll('p')])
|
||||||
@@ -23,31 +26,37 @@ def processor():
|
|||||||
|
|
||||||
@app.template_filter('category_title')
|
@app.template_filter('category_title')
|
||||||
def category_title(category_id: str) -> str:
|
def category_title(category_id: str) -> str:
|
||||||
with open(path.join(md_directory, 'categories.json')) as categories_file:
|
''' Jninja filter to get a category title by its ID '''
|
||||||
|
with open(path.join(md_directory, 'categories.json'), encoding='utf8') as categories_file:
|
||||||
categories = json.load(categories_file)
|
categories = json.load(categories_file)
|
||||||
|
|
||||||
return categories.get(category_id).get('title', '')
|
return categories.get(category_id).get('title', '')
|
||||||
|
|
||||||
@app.template_filter('human_date')
|
@app.template_filter('human_date')
|
||||||
def human_date(iso_date: str) -> str:
|
def human_date(iso_date: str) -> str:
|
||||||
|
''' Jninja filter to convert an ISO date to human readable. '''
|
||||||
try:
|
try:
|
||||||
return datetime.fromisoformat(str(iso_date)).strftime('%A %d %B %Y')
|
return datetime.fromisoformat(str(iso_date)).strftime('%A %d %B %Y')
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return iso_date
|
return iso_date
|
||||||
|
|
||||||
@app.template_filter('to_html')
|
@app.template_filter('to_html')
|
||||||
def to_html(content):
|
def to_html(content: str) -> str:
|
||||||
|
''' Jninja filter to wrap markdown '''
|
||||||
return markdown(content)
|
return markdown(content)
|
||||||
|
|
||||||
def get_all_posts(directory: str) -> list:
|
def get_all_posts(directory: str = md_directory) -> list:
|
||||||
|
''' Get all posts in the posts directory '''
|
||||||
abs_paths = [path.join(directory, x) for x in glob(f'{directory}/*.md')]
|
abs_paths = [path.join(directory, x) for x in glob(f'{directory}/*.md')]
|
||||||
return [frontmatter.load(x) for x in abs_paths]
|
return [frontmatter.load(x) for x in abs_paths]
|
||||||
|
|
||||||
def get_by_meta_key(directory: str, key: str, value: str):
|
def get_by_meta_key(directory: str, key: str, value: str) -> list:
|
||||||
|
''' Get posts by a metadata key value pair '''
|
||||||
return [x for x in get_all_posts(directory) if x.get(key) == value or type(x.get(key, [])) is list and value in x.get(key, [])]
|
return [x for x in get_all_posts(directory) if x.get(key) == value or type(x.get(key, [])) is list and value in x.get(key, [])]
|
||||||
|
|
||||||
@app.route('/projects/')
|
@app.route('/projects/')
|
||||||
def projects():
|
def projects() -> str:
|
||||||
|
''' Load the projects page '''
|
||||||
articles_to_return = sorted(
|
articles_to_return = sorted(
|
||||||
get_all_posts(
|
get_all_posts(
|
||||||
md_directory),
|
md_directory),
|
||||||
@@ -60,7 +69,7 @@ def projects():
|
|||||||
error='There\'s nothing here... yet.',
|
error='There\'s nothing here... yet.',
|
||||||
description='I\'m still working on this page. Check back soon for some content.')
|
description='I\'m still working on this page. Check back soon for some content.')
|
||||||
try:
|
try:
|
||||||
with open(path.join(md_directory, 'categories.json')) as categories_file:
|
with open(path.join(md_directory, 'categories.json'), encoding='utf8') as categories_file:
|
||||||
categories = json.load(categories_file)
|
categories = json.load(categories_file)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return render_template('error.html',
|
return render_template('error.html',
|
||||||
@@ -71,14 +80,16 @@ def projects():
|
|||||||
articles=articles_to_return,
|
articles=articles_to_return,
|
||||||
all_categories=categories,
|
all_categories=categories,
|
||||||
title='Projects',
|
title='Projects',
|
||||||
|
page_title='Projects - ',
|
||||||
description='A selection of projects I\'ve been involved in')
|
description='A selection of projects I\'ve been involved in')
|
||||||
|
|
||||||
@app.route('/projects/category/<category>/')
|
@app.route('/projects/category/<category_id>/')
|
||||||
def category(category):
|
def category(category_id: str) -> str:
|
||||||
|
''' Load the page for a given category '''
|
||||||
try:
|
try:
|
||||||
with open(path.join(md_directory, 'categories.json')) as categories_file:
|
with open(path.join(md_directory, 'categories.json'), encoding='utf8') as categories_file:
|
||||||
categories = json.load(categories_file)
|
categories = json.load(categories_file)
|
||||||
the_category = categories.get(category)
|
the_category = categories.get(category_id)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return render_template('error.html',
|
return render_template('error.html',
|
||||||
error='There\'s nothing here... yet.',
|
error='There\'s nothing here... yet.',
|
||||||
@@ -89,7 +100,7 @@ def category(category):
|
|||||||
|
|
||||||
articles_to_return = sorted(
|
articles_to_return = sorted(
|
||||||
get_by_meta_key(
|
get_by_meta_key(
|
||||||
md_directory, 'categories', category),
|
md_directory, 'categories', category_id),
|
||||||
key=lambda d: d.metadata.get('date'),
|
key=lambda d: d.metadata.get('date'),
|
||||||
reverse=True
|
reverse=True
|
||||||
)
|
)
|
||||||
@@ -104,11 +115,12 @@ def category(category):
|
|||||||
description=the_category['long_description'],
|
description=the_category['long_description'],
|
||||||
page_title=f'{the_category["title"]} - ',
|
page_title=f'{the_category["title"]} - ',
|
||||||
all_categories=categories,
|
all_categories=categories,
|
||||||
current_category=category)
|
current_category=category_id)
|
||||||
|
|
||||||
@app.route('/projects/<article>')
|
@app.route('/projects/<article_id>')
|
||||||
def article(article):
|
def article(article_id: str) -> str:
|
||||||
articles = get_by_meta_key(md_directory, 'id', article)
|
''' Load a single article '''
|
||||||
|
articles = get_by_meta_key(md_directory, 'id', article_id)
|
||||||
|
|
||||||
if len(articles) == 0:
|
if len(articles) == 0:
|
||||||
return Response(status=404)
|
return Response(status=404)
|
||||||
@@ -120,6 +132,36 @@ def article(article):
|
|||||||
metadata=the_article.metadata,
|
metadata=the_article.metadata,
|
||||||
page_title=f'{the_article.metadata["title"]} - ')
|
page_title=f'{the_article.metadata["title"]} - ')
|
||||||
|
|
||||||
@app.route('/projects/image/<image>')
|
@app.route('/projects/image/<image_name>')
|
||||||
def image(image):
|
def image(image_name: str) -> Response:
|
||||||
return send_from_directory(path.join(md_directory, 'images'), image)
|
''' Resize and return an image. '''
|
||||||
|
w = int(request.args.get('w', 0))
|
||||||
|
h = int(request.args.get('h', 0))
|
||||||
|
|
||||||
|
if w == 0 and h == 0:
|
||||||
|
return send_from_directory(md_directory, path.join('images', image_name))
|
||||||
|
try:
|
||||||
|
the_image = Image.open(path.join(md_directory, 'images', image_name))
|
||||||
|
except FileNotFoundError:
|
||||||
|
return Response(status=404)
|
||||||
|
except UnidentifiedImageError:
|
||||||
|
return send_from_directory(md_directory, path.join('images', image_name))
|
||||||
|
|
||||||
|
max_width, max_height = the_image.size
|
||||||
|
|
||||||
|
if (w >= max_width and h >= max_height):
|
||||||
|
return send_from_directory(md_directory, path.join('images', image_name))
|
||||||
|
|
||||||
|
req_size = [max_width, max_height]
|
||||||
|
if w > 0:
|
||||||
|
req_size[0] = w
|
||||||
|
if h > 0:
|
||||||
|
req_size[1] = h
|
||||||
|
|
||||||
|
resized_img = BytesIO()
|
||||||
|
the_image.thumbnail(tuple(req_size))
|
||||||
|
the_image.save(resized_img, format=the_image.format)
|
||||||
|
|
||||||
|
response = make_response(resized_img.getvalue())
|
||||||
|
response.headers.set('Content-Type', f'image/{the_image.format}')
|
||||||
|
return response
|
||||||
|
|||||||
Regular → Executable
+3
@@ -4,3 +4,6 @@ import sys
|
|||||||
sys.path.append('/var/www/jc')
|
sys.path.append('/var/www/jc')
|
||||||
|
|
||||||
from index import app as application
|
from index import app as application
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
application.run(debug=True)
|
||||||
Regular → Executable
+3
@@ -1,6 +1,9 @@
|
|||||||
|
setuptools>=78.1.1
|
||||||
flask>=2.2.3
|
flask>=2.2.3
|
||||||
flask-markdown>=0.3
|
flask-markdown>=0.3
|
||||||
markdown>=3.4.1
|
markdown>=3.4.1
|
||||||
beautifulsoup4>=4.11.1
|
beautifulsoup4>=4.11.1
|
||||||
python-frontmatter>=1.1.0
|
python-frontmatter>=1.1.0
|
||||||
requests>=2.32.3
|
requests>=2.32.3
|
||||||
|
pillow>=11.0.0
|
||||||
|
mod_wsgi>=5.0.2
|
||||||
|
|||||||
Executable
+43
@@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import json
|
||||||
|
from flask import url_for, request, Response
|
||||||
|
from re import match
|
||||||
|
from index import app
|
||||||
|
from projects import get_all_posts
|
||||||
|
|
||||||
|
def get_routes() -> list:
|
||||||
|
routes = []
|
||||||
|
for rule in app.url_map.iter_rules():
|
||||||
|
if 0 >= len(rule.arguments):
|
||||||
|
url = url_for(rule.endpoint, **(rule.defaults or {}))
|
||||||
|
routes.append(url)
|
||||||
|
return routes
|
||||||
|
|
||||||
|
def get_build_date():
|
||||||
|
try:
|
||||||
|
with open('/var/www/jc/.buildinfo.json', encoding='utf8') as build:
|
||||||
|
build_json = json.load(build)
|
||||||
|
return build_json['date']
|
||||||
|
except:
|
||||||
|
return '1970-01-01'
|
||||||
|
|
||||||
|
@app.route('/sitemap.xml')
|
||||||
|
def sitemap():
|
||||||
|
date = get_build_date()
|
||||||
|
root = ET.Element('urlset', xmlns='http://www.sitemaps.org/schemas/sitemap/0.9')
|
||||||
|
base_url = match(r'^https?:\/\/.+:?\d*(?=\/)', request.base_url).group()
|
||||||
|
base_url = base_url.replace('http://', 'https://')
|
||||||
|
for route in get_routes():
|
||||||
|
url = ET.SubElement(root, 'url')
|
||||||
|
ET.SubElement(url, 'loc').text = base_url + route
|
||||||
|
ET.SubElement(url, 'lastmod').text = date
|
||||||
|
for article in get_all_posts():
|
||||||
|
if 'link' in article.metadata:
|
||||||
|
continue
|
||||||
|
url = ET.SubElement(root, 'url')
|
||||||
|
ET.SubElement(url, 'loc').text = f'{base_url}/projects/{article.metadata['id']}'
|
||||||
|
ET.SubElement(url, 'lastmod').text = article.metadata['date'].strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
return Response(ET.tostring(root, encoding='utf-8'), 200, {'content-type': 'application/xml'})
|
||||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
BIN
Binary file not shown.
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
Regular → Executable
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Regular → Executable
Regular → Executable
Regular → Executable
Executable
+4
@@ -0,0 +1,4 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://jakecharman.co.uk/sitemap.xml
|
||||||
Regular → Executable
+53
-3
@@ -1,22 +1,25 @@
|
|||||||
@media (min-width: 1000px) {
|
@media (min-width: 1000px) {
|
||||||
#technology{
|
#technology{
|
||||||
background-position-x: 35vw;
|
background-position-x: 35vw;
|
||||||
height: 75vh;
|
height: fit-content;
|
||||||
min-height: 75vh;
|
min-height: 75vh;
|
||||||
|
background-position-y: 780px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#motorsport{
|
#motorsport{
|
||||||
background-position-y: -180px;
|
background-position-y: -180px;
|
||||||
background-position-x: -200px;
|
background-position-x: -200px;
|
||||||
background-size: auto;
|
background-size: auto;
|
||||||
|
height: fit-content;
|
||||||
min-height: 75vh;
|
min-height: 75vh;
|
||||||
height: 75vh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.text{
|
.text{
|
||||||
width: 50%;
|
width: 50%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
height: fit-content;
|
||||||
|
min-height: 75vh
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-right{
|
.text-right{
|
||||||
@@ -29,6 +32,13 @@
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gradient {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
background-image: linear-gradient(to top, rgba(23, 22, 20, 1) 70%, rgba(23, 22, 20, 0));
|
||||||
|
min-height: 75vh;
|
||||||
|
}
|
||||||
|
|
||||||
.gradient-left{
|
.gradient-left{
|
||||||
background-image: linear-gradient(to right, rgba(23, 22, 20, 1), rgba(23, 22, 20, 1), rgba(23, 22, 20, 0));
|
background-image: linear-gradient(to right, rgba(23, 22, 20, 1), rgba(23, 22, 20, 1), rgba(23, 22, 20, 0));
|
||||||
}
|
}
|
||||||
@@ -42,6 +52,8 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
margin: 0 auto 0 auto;
|
||||||
|
width: 80vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project{
|
.project{
|
||||||
@@ -53,10 +65,48 @@
|
|||||||
float: right;
|
float: right;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
|
||||||
#logo-container{
|
#logo-container{
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 25vh;
|
height: 25vh;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
#article>p>img{
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#certs>a {
|
||||||
|
max-width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#techlogos {
|
||||||
|
max-height: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#techlogos>a{
|
||||||
|
max-width: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt {
|
||||||
|
width: 74%;
|
||||||
|
height: 40vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery {
|
||||||
|
display: flex;
|
||||||
|
height: 35vh;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery * {
|
||||||
|
height: inherit !important;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Regular → Executable
+80
-11
@@ -13,13 +13,17 @@ h1, h2, h3{
|
|||||||
}
|
}
|
||||||
|
|
||||||
header{
|
header{
|
||||||
background-color: 4c4c4c;
|
background-color: #4c4c4c;
|
||||||
height: 25vh;
|
height: 25vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main{
|
||||||
|
min-height: 65vh;
|
||||||
|
}
|
||||||
|
|
||||||
footer{
|
footer{
|
||||||
background-color: 4c4c4c;
|
background-color: #4c4c4c;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -48,33 +52,32 @@ footer h2, section h2{
|
|||||||
|
|
||||||
#technology{
|
#technology{
|
||||||
background-image: url(../images/njr-code.png);
|
background-image: url(../images/njr-code.png);
|
||||||
height: 100vh;
|
height: fit-content;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-position-x: -400px;
|
background-position-x: -400px;
|
||||||
|
background-position-y: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#motorsport{
|
#motorsport{
|
||||||
background-image: url(../images/topfuel_startline.jpg.jpeg);
|
background-image: url(../images/topfuel_startline.jpg.jpeg);
|
||||||
height: 100vh;
|
height: fit-content;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-position-x: -65px;
|
background-position-x: -65px;
|
||||||
background-position-y: -90px;
|
background-position-y: -100px;
|
||||||
background-size: 200%;
|
background-size: 200%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gradient{
|
.gradient{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: fit-content;
|
||||||
background-image: linear-gradient(to top, rgba(23, 22, 20, 1) 70%, rgba(23, 22, 20, 0));
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.text{
|
.text{
|
||||||
height: 70%;
|
height: fit-content;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
background-color: rgba(23, 22, 20, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.text>div{
|
.text>div{
|
||||||
@@ -118,7 +121,9 @@ a{
|
|||||||
|
|
||||||
.project-thumb{
|
.project-thumb{
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 100%;
|
max-height: 50%;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#filter {
|
#filter {
|
||||||
@@ -203,4 +208,68 @@ label{
|
|||||||
|
|
||||||
#contact-error{
|
#contact-error{
|
||||||
color: red;
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
#article{
|
||||||
|
padding: 0 10px 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#article>p>img{
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto 0 auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre{
|
||||||
|
overflow-x: auto;
|
||||||
|
background-color: #36332f;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#certs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#certs>a {
|
||||||
|
max-width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#certs>a>img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#techlogos {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
max-height:30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#techlogos>a {
|
||||||
|
max-width: 10%;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#techlogos>a>img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
height: 30vh;
|
||||||
|
width: 100%;
|
||||||
|
background-image: linear-gradient(to top, rgba(23, 22, 20, 1) 10%, rgba(23, 22, 20, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt {
|
||||||
|
width: 100%;
|
||||||
|
height: 22vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery * {
|
||||||
|
padding: 10px;
|
||||||
|
max-width: 80vw;
|
||||||
}
|
}
|
||||||
Regular → Executable
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{% include 'header.html' %}
|
{% include 'header.html' %}
|
||||||
<main>
|
<main>
|
||||||
<section id="tech-article">
|
<section id="article">
|
||||||
<h1>{{ metadata.title}} </h1>
|
<h1>{{ metadata.title}} </h1>
|
||||||
<p>{{ metadata.date | human_date }}</p>
|
<p>{{ metadata.date | human_date }}</p>
|
||||||
<hr />
|
<hr />
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
+10
@@ -2,6 +2,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="Personal website of Jake Charman. A technology professional based in the UK.">
|
||||||
<title>{{ page_title }}Jake Charman</title>
|
<title>{{ page_title }}Jake Charman</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Orbitron:[email protected]&family=Tourney:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Orbitron:[email protected]&family=Tourney:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&family=Orbitron:[email protected]&family=Tourney:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&family=Orbitron:[email protected]&family=Tourney:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
|
||||||
@@ -10,9 +11,18 @@
|
|||||||
<link rel="stylesheet" href="/static/fonts/fontawesome/css/all.min.css" />
|
<link rel="stylesheet" href="/static/fonts/fontawesome/css/all.min.css" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link rel="icon" type="image/x-icon" href="/static/images/jc.ico">
|
||||||
<script src="/static/js/filter_projects.js"></script>
|
<script src="/static/js/filter_projects.js"></script>
|
||||||
<script src="/static/js/update_copyright.js"></script>
|
<script src="/static/js/update_copyright.js"></script>
|
||||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||||
|
<script async src="https://www.googletagmanager.com/gtag/js?id=G-6WMXXY0RL0"></script>
|
||||||
|
<script>
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
|
||||||
|
gtag('config', 'G-6WMXXY0RL0');
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
|
|||||||
Regular → Executable
+44
-11
@@ -2,18 +2,46 @@
|
|||||||
<main>
|
<main>
|
||||||
<section id="technology">
|
<section id="technology">
|
||||||
<div class="gradient gradient-left">
|
<div class="gradient gradient-left">
|
||||||
|
<div class="spacer"></div>
|
||||||
<div class="text text-left">
|
<div class="text text-left">
|
||||||
<div>
|
<div>
|
||||||
<h2>Technology</h2>
|
<h2>Technology</h2>
|
||||||
<hr>
|
<hr>
|
||||||
<p>Working with technology is my day job, I currently specialise in:</p>
|
<p>Technology is my day job. I design and build distributed systems for large companies around the world.</p>
|
||||||
<ul>
|
<p>
|
||||||
<li>Linux (primarily RHEL & Debian based)</li>
|
I also work with technology in my spare time, building systems for <a href="#motorsport">Nitro Junkie</a>, and for me to use personally. For example, the current iteration of this website is written in Python and deployed via a Jenkins pipeline to Google Cloud Run as a Docker container.
|
||||||
<li>SCM with Git and Subversion</li>
|
</p>
|
||||||
<li>Big Data (Hadoop & Cloud storage)</li>
|
<p>
|
||||||
<li>Programming (Python, Bash & C#)</li>
|
Below are some of the technologies I'm using most at the moment.
|
||||||
</ul>
|
My personal projects can also be found over on the <a href="/projects/">projects</a> page. Be aware though that if I'm able to write about a project here, it most likely wasn't done professionally so some may be a little rough around the edges.
|
||||||
<p>I also run some services for personal use and occasionally write software. I may write about some of the services I run in the future. For now, my code can be found on <a href="https://github.com/jcharman">GitHub</a>.</p>
|
</p>
|
||||||
|
<div id="techlogos">
|
||||||
|
<a href="https://ansible.com"><img src="/static/images/technology/ansible.png" /></a>
|
||||||
|
<a href="https://aws.amazon.com/"><img src="/static/images/technology/aws.png" /></a>
|
||||||
|
<a href="https://azure.microsoft.com/en-gb"><img src="/static/images/technology/azure.png" /></a>
|
||||||
|
<a href="https://dotnet.microsoft.com/en-us/languages/csharp"><img src="/static/images/technology/csharp.png" /></a>
|
||||||
|
<a href="https://www.debian.org/"><img src="/static/images/technology/debian.png" /></a>
|
||||||
|
<a href="https://www.digitalocean.com/"><img src="/static/images/technology/digitalocean.png" /></a>
|
||||||
|
<a href="https://www.docker.com/"><img src="/static/images/technology/docker.png" /></a>
|
||||||
|
<a href="https://www.freeipa.org/"><img src="/static/images/technology/freeipa.png" /></a>
|
||||||
|
<a href="https://cloud.google.com/"><img src="/static/images/technology/gcloud.png" /></a>
|
||||||
|
<a href="https://git-scm.com/"><img src="/static/images/technology/git.png" /></a>
|
||||||
|
<a href="https://www.grafana.com/"><img src="/static/images/technology/grafana.png" /></a>
|
||||||
|
<a href="https://hadoop.apache.org/"><img src="/static/images/technology/hadoop.svg" /></a>
|
||||||
|
<a href="https://hive.apache.org/"><img src="/static/images/technology/hive.svg" /></a>
|
||||||
|
<a href="https://www.java.com/"><img src="/static/images/technology/java.png" /></a>
|
||||||
|
<a href="https://www.jenkins.io/"><img src="/static/images/technology/jenkins.png" /></a>
|
||||||
|
<a href="https://mariadb.org/"><img src="/static/images/technology/mariadb.png" /></a>
|
||||||
|
<a href="https://nginx.org/"><img src="/static/images/technology/nginx.svg" /></a>
|
||||||
|
<a href="https://www.proxmox.com/"><img src="/static/images/technology/proxmox.png" /></a>
|
||||||
|
<a href="https://www.python.org/"><img src="/static/images/technology/python.png" /></a>
|
||||||
|
<a href="https://www.redhat.com/en/technologies/linux-platforms/enterprise-linux"><img src="/static/images/technology/redhat.png" /></a>
|
||||||
|
<a href="https://rockylinux.org/"><img src="/static/images/technology/rocky.png" /></a>
|
||||||
|
<a href="https://subversion.apache.org/"><img src="/static/images/technology/svn.svg" /></a>
|
||||||
|
</div>
|
||||||
|
<div id="certs">
|
||||||
|
<a href="https://catalog-education.oracle.com/ords/certview/sharebadge?id=A6C9B3D3D21627EB9C2504395194FAF772E01412911D4ADC78063133D54579ED"><img src="/static/images/certs/OCIF2023CA.png" /></a>
|
||||||
|
</div>
|
||||||
<div class="social">
|
<div class="social">
|
||||||
<a class="button" href="https://www.linkedin.com/in/jakecharman/"><i class="fa-brands fa-linkedin-in"></i></a>
|
<a class="button" href="https://www.linkedin.com/in/jakecharman/"><i class="fa-brands fa-linkedin-in"></i></a>
|
||||||
<a class="button" href="https://github.com/jcharman/"><i class="fa-brands fa-github"></i></a>
|
<a class="button" href="https://github.com/jcharman/"><i class="fa-brands fa-github"></i></a>
|
||||||
@@ -24,13 +52,18 @@
|
|||||||
</section>
|
</section>
|
||||||
<section id="motorsport">
|
<section id="motorsport">
|
||||||
<div class="gradient gradient-right">
|
<div class="gradient gradient-right">
|
||||||
|
<div class="spacer"></div>
|
||||||
<div class="text text-right">
|
<div class="text text-right">
|
||||||
<div>
|
<div>
|
||||||
<h2>Racing</h2>
|
<h2>Racing</h2>
|
||||||
<hr />
|
<hr />
|
||||||
<p>When not working on tech, I can usually be found at <a href="https://santapod.com">Santa Pod Raceway</a> working on my Dad's <a href="https://nitrojunkie.uk">NitroJunkie.UK</a> "All In" Top Fuel Bike. The bike is purpose built from the ground up with a supercharged, nitromethane injected engine capable of producing approximately 1000 horsepower. </p>
|
<p>When I'm not at work, I can often be found at <a href="https://santapod.com">Santa Pod Raceway</a> with <a href="https://nitrojunkie.uk">Nitro Junkie Racing</a></p>
|
||||||
<p>The photo for this section was taken by <a href="https://www.facebook.com/BlackettPhotography">Blackett Photography</a></p>
|
<p>We run a Top Fuel motorcycle purpose built for drag racing. The engine is loosely based on a Kawasaki Z 1000, but supercharged, and nitromethane injected to produce around 1,000 horsepower.</p>
|
||||||
<p>You can read more about our racing at <a href="https://nitrojunkie.uk">nitrojunkie.uk</a></p>
|
<p>I've also had the opportunity to work on a 10,000 horsepower Top Fuel Dragster. I <a href="https://nitrojunkie.uk/news/post/10000_horses">wrote about this on the Nitro Junkie website.</a></p>
|
||||||
|
<div class="gallery">
|
||||||
|
<iframe class="yt" src="https://www.youtube.com/embed/Wa-V8mQTXFk?si=W-LzfR3Qj_jgAdkY&clip=UgkxV9lBj4pP1cvnR5seb82RQpWeE7RdnOXB&clipt=EOrWtgsYyqu6Cw&autoplay=1&mute=1" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||||
|
<img src="/static/images/jake_tf_1.jpg">
|
||||||
|
</div>
|
||||||
<div class="social">
|
<div class="social">
|
||||||
<a class="button" href="https://nitrojunkie.uk"><i class="fa-solid fa-globe"></i></a>
|
<a class="button" href="https://nitrojunkie.uk"><i class="fa-solid fa-globe"></i></a>
|
||||||
<a class="button" href="https://www.youtube.com/@NitroJunkieUK"><i class="fa-brands fa-youtube"></i></a>
|
<a class="button" href="https://www.youtube.com/@NitroJunkieUK"><i class="fa-brands fa-youtube"></i></a>
|
||||||
|
|||||||
Regular → Executable
+21
-6
@@ -12,20 +12,35 @@
|
|||||||
</nav>
|
</nav>
|
||||||
<h2>{{ title }}</h2>
|
<h2>{{ title }}</h2>
|
||||||
<p>{{ description }}</p>
|
<p>{{ description }}</p>
|
||||||
|
<p style="text-align: center; font-weight: bold; font-style: italic;">This page is relatively new. I'm adding projects as fast as I can but feel free to <a href="/contact">contact me</a> if you want to know about something in particular.</p>
|
||||||
<section id="projects">
|
<section id="projects">
|
||||||
{% for row in articles %}
|
{% for row in articles %}
|
||||||
<div class="project">
|
<div class="project">
|
||||||
<img class="project-thumb" src="/projects/image/{{ row.image }}">
|
{% if row.get('link') is not none %}
|
||||||
|
<a href="{{ row.link }}">
|
||||||
|
{% else %}
|
||||||
|
<a href="/projects/{{ row.id }}">
|
||||||
|
{% endif %}
|
||||||
|
<img class="project-thumb"
|
||||||
|
srcset="
|
||||||
|
{% for i in range(200, 5100, 100) %}
|
||||||
|
/projects/image/{{ row.image }}?w={{i}} {{i}}w{{"," if not loop.last}}
|
||||||
|
{% endfor %}
|
||||||
|
"
|
||||||
|
sizes="
|
||||||
|
(max-width: 999px) 80vw,
|
||||||
|
(min-width: 1000px) 20vw
|
||||||
|
"
|
||||||
|
src="/projects/image/{{ row.image }}">
|
||||||
|
</a>
|
||||||
<div class="project-text">
|
<div class="project-text">
|
||||||
{% if row.get('link') is not none %}
|
{% if row.get('link') is not none %}
|
||||||
<a href="{{ row.link }}">
|
<a href="{{ row.link }}">
|
||||||
<h3>{{ row.title }}</h3>
|
|
||||||
</a>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="/projects/{{ row.id }}">
|
<a href="/projects/{{ row.id }}">
|
||||||
|
{% endif %}
|
||||||
<h3>{{ row.title }}</h3>
|
<h3>{{ row.title }}</h3>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
|
||||||
<p class="article-description">{{ row.description }}</p>
|
<p class="article-description">{{ row.description }}</p>
|
||||||
<p class="article-date">{{ row.date | human_date }}</p>
|
<p class="article-date">{{ row.date | human_date }}</p>
|
||||||
{% for category in row.categories %}
|
{% for category in row.categories %}
|
||||||
|
|||||||
Reference in New Issue
Block a user