From d00d9dbfc5d0259e10ffbf976dc8f95e74b9a037 Mon Sep 17 00:00:00 2001
From: Anson Biggs
Date: Mon, 1 Dec 2025 22:01:58 -0700
Subject: [PATCH] Fully Integrate into Ghost
---
Anson-Projects/projects/.gitlab-ci.yml | 25 +-
.../index/execute-results/html.json | 4 +-
.../index/execute-results/html.json | 4 +-
.../quarto-listing/quarto-listing.js | 3 +-
.../index/execute-results/html.json | 4 +-
.../index/execute-results/html.json | 4 +-
Anson-Projects/projects/_quarto.yml | 75 +-
.../projects/ghost-upload/.env.example | 2 +
.../projects/ghost-upload/.gitignore | 5 +
.../projects/ghost-upload/.gitlab-ci.yml | 5 +-
.../projects/ghost-upload/Cargo.lock | 633 ++--
.../projects/ghost-upload/Cargo.toml | 34 +-
.../projects/ghost-upload/README.md | 11 +-
.../projects/ghost-upload/src/main.rs | 3249 +++++++++++++++--
.../index.qmd | 15 +-
.../index.qmd | 14 +-
.../index.qmd | 2 +-
.../pendulum-preview.gif | Bin 0 -> 564543 bytes
18 files changed, 3459 insertions(+), 630 deletions(-)
create mode 100644 Anson-Projects/projects/ghost-upload/.env.example
create mode 100644 Anson-Projects/projects/ghost-upload/.gitignore
create mode 100644 Anson-Projects/projects/posts/2025-05-10-double-pendulum-redux/pendulum-preview.gif
diff --git a/Anson-Projects/projects/.gitlab-ci.yml b/Anson-Projects/projects/.gitlab-ci.yml
index f654903b3..7ace71f67 100644
--- a/Anson-Projects/projects/.gitlab-ci.yml
+++ b/Anson-Projects/projects/.gitlab-ci.yml
@@ -1,5 +1,13 @@
-build:
- stage: build
+workflow:
+ rules:
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
+ when: never
+ - when: always
+
+build-base-image:
+ stage: .pre
+ timeout: 2 hours
+ needs: []
image:
name: gcr.io/kaniko-project/executor:v1.23.2-debug
entrypoint: [""]
@@ -7,13 +15,22 @@ build:
- /kaniko/executor
--context "${CI_PROJECT_DIR}"
--dockerfile "${CI_PROJECT_DIR}/Dockerfile"
- --destination "${CI_REGISTRY_IMAGE}:${CI_COMMIT_BRANCH}"
+ --destination "${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}"
--cleanup
+ - echo "Image pushed as ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}"
+ rules:
+ - when: manual
+ allow_failure: true
+
+# Update this hash after running the manual build job
+.base_image: &base_image "registry.gitlab.com/anson-projects/projects:497a06e"
staging:
stage: deploy
- image: ${CI_REGISTRY_IMAGE}:${CI_COMMIT_BRANCH}
+ image: *base_image
script:
+ - echo "Ensuring Julia packages are up to date..."
+ - julia --project=. -e "using Pkg; Pkg.instantiate()"
- echo "Building the main website with Quarto..."
- quarto render --to html --output-dir public
- echo "Checking for RSS feed after render..."
diff --git a/Anson-Projects/projects/.quarto/_freeze/posts/2021-04-01-air-propulsion-simulation/index/execute-results/html.json b/Anson-Projects/projects/.quarto/_freeze/posts/2021-04-01-air-propulsion-simulation/index/execute-results/html.json
index e0cb2087b..905e1c35e 100644
--- a/Anson-Projects/projects/.quarto/_freeze/posts/2021-04-01-air-propulsion-simulation/index/execute-results/html.json
+++ b/Anson-Projects/projects/.quarto/_freeze/posts/2021-04-01-air-propulsion-simulation/index/execute-results/html.json
@@ -1,8 +1,8 @@
{
- "hash": "f949d3b331ebc31b94d98d3f77365500",
+ "hash": "e86f7f84efe6d892be0f803d1a118411",
"result": {
"engine": "jupyter",
- "markdown": "---\ntitle: \"Air Propulsion Simulation\"\ndescription: |\n Simulating the performance of a compressed air propulsion system as an alternative to solid rocket motors using Julia.\ndescription-meta: |\n Simulate air propulsion for lunar mining transport! This project explores using compressed air as an alternative to solid rocket motors. See the Julia simulation results and comparisons with traditional rocket motor performance. Explore the code and learn more about this innovative approach.\nrepository_url: https://gitlab.com/lander-team/air-prop-simulation\ndate: 2021-04-01\ndate-modified: 2024-03-10\ncategories:\n - Julia\n - Capstone\n - University\n - Code\n - Aerospace\n - Math\ncreative_commons: CC BY\nbanner: prop_comp.png\nimage-alt: A line graph comparing the thrust of different propulsion systems over time. The x-axis represents time in seconds, and the y-axis represents thrust in Newtons. The graph displays the thrust curves for Air Propulsion, F10, F15, and G8ST.\nformat:\n html:\n code-tools: true\n code-fold: false\nexecute:\n output: false\nfreeze: true\n---\n\n\n\n\nFor Capstone my team was tasked with designing a system capable of moving mining equipment and materials around the surface of the Moon using a propulsive landing. The system had to be tested on Earth with something feasible for our team to build in 2 semesters. One of the first considerations my capstone advisor wanted was to test the feasibility of an air propulsion system instead of the obvious solution that of using solid rocket motors. This document is just _napkin math_ to determine if the system is even feasibly and is not meant to be a rigorous study of an air propulsion system that would easily keep a capstone team busy by itself. \n\n::: {#c8d415f6 .cell execution_count=1}\n``` {.julia .cell-code code-fold=\"true\" code-summary=\"Imports\"}\nusing Plots\nplotlyjs()\n\nusing Unitful\nusing DataFrames\nusing Measurements\nusing Measurements: value, uncertainty\nusing CSV\n```\n:::\n\n\n## The Simulation\n\nI chose an off-the-shelf paintball gun tank for the pressure vessel. The primary consideration was the incredible pressure to weight ratio, and the fact that it is designed to be bumped around would be necessary for proving the safety of the system further into the project. \n\n::: {#aecb6e1d .cell execution_count=2}\n``` {.julia .cell-code code-fold=\"false\"}\n# Tank https://www.amazon.com/Empire-Paintball-BASICS-Pressure-Compressed/dp/B07B6M48SR/\nV = (85 ± 5)u\"inch^3\";\nP0 = (4200.0 ± 300)u\"psi\";\nWtank = (2.3 ± 0.2)u\"lb\";\nPmax = (250 ± 50)u\"psi\"; # Max Pressure that can come out the nozzle\n```\n:::\n\n\nThe nozzle diameter was changed until the air prop system had a _burn time_ similar to a G18ST rocket motor. The propulsion system's total impulse is not dependant on the nozzle diameter, so this was just done to make it plot nicely with the rest of the rocket motors since, at this time, it is unknown what the optimal thrust profile is. \n\n::: {#10781a1e .cell execution_count=3}\n``` {.julia .cell-code}\n# Params\nd_nozzle = ((1 // 18) ± 0.001)u\"inch\";\na_nozzle = (pi / 4) * d_nozzle^2;\n```\n:::\n\n\nThese are just universal values for what a typical day would look like during the summer in Northern Arizona. [@cengel_thermodynamics]\n\n::: {#4b47d237 .cell execution_count=4}\n``` {.julia .cell-code}\n# Universal Stuff\nP_amb = (1 ± 0.2)u\"atm\";\nγ = 1.4 ± 0.05;\nR = 287.05u\"J/(kg * K)\";\nT = (300 ± 20)u\"K\";\n```\n:::\n\n\nThe actual simulation is quite simple. The basic idea is that using the current pressure, you can calculate $\\dot{m}$, which allows calculating the Thrust, and then you can subtract the current mass of air in the tank by $\\dot{m}$ and recalculate pressure using the new mass then repeat the whole process.\n\n\nThe bulk of the equations in the simulation came from [@cengel_thermodynamics], while the Thrust and $v_e$ equations came from [@sutton_rocket_2001, eq: 2-14].\n\n$$ T = \\dot{m} \\cdot v_\\text{Exit} + A_\\text{Nozzle} \\cdot (P - P_\\text{Ambient}) $$\n\nThe initial pressure difference is 4190.0 ± 300.0 psi, which is massive, so the area of the nozzle significantly alters the thrust profile. The paintball tanks come with pressure regulators, in our case, 800 psi which is still a huge number compared to atmospheric pressure. While the total impulse of the system doesn't change with different nozzle areas, the peak thrust and _burn time_ vary greatly. One of the benefits of doing air propulsion and the reason it was even considered so seriously is that it should be possible to change the nozzle diameter in flight, allowing thrust to be throttled, making controlled landing easier to control. \n\n::: {#1340ebc6 .cell execution_count=5}\n``` {.julia .cell-code}\nt = 0.0u\"s\";\nP = P0 |> u\"Pa\";\nM = V * (P / (R * T)) |> u\"kg\";\nts = 1u\"ms\";\ndf = DataFrame(Thrust=(0 ± 0)u\"N\", Pressure=P0, Time=0.0u\"s\", Mass=M);\nwhile M > 0.005u\"kg\"\n # Calculate what is leaving tank\n P = minimum([P, Pmax])\n ve = sqrt((2 * γ / (γ - 1)) * R * T * (1 - P_amb / P)^((γ - 1) / γ)) |> u\"m/s\"\n ρ = P / (R * T) |> u\"kg/m^3\"\n ṁ = ρ * a_nozzle * ve |> u\"kg/s\"\n\n Thrust = ṁ * ve + a_nozzle * (P - P_amb) |> u\"N\"\n\n # Calculate what is still in the tank\n M = M - ṁ * ts |> u\"kg\"\n P = (M * R * T) / V |> u\"Pa\"\n t = t + ts\n\n df_step = DataFrame(Thrust=Thrust, Pressure=P, Time=t, Mass=M)\n append!(df, df_step)\nend\n```\n:::\n\n\n## Analysis\n\n\nBelow in figure 1, the result of the simulation is plotted. Notice the massive error once the tank starts running low. This is because the calculation for pressure has a lot of very uncertain variables. This is primarily due to air being a compressible fluid, making this simulation challenging to do accurately. The thrust being below 0 N might not make intuitive sense, but it's technically possible for the pressure to compress, leaving the inside of the rocket nozzle with a pressure that's actually below atmospheric pressure. The effect would likely last a fraction of a second, but the point stands that this simulation is wildly inaccurate and only meant to get an idea of what an air propulsion system is capable of. \n\n::: {#c6959066 .cell class-output='preview-image' code-folding='true' execution_count=6}\n``` {.julia .cell-code}\nthrust_values = df.Thrust .|> ustrip .|> value;\nthrust_uncertainties = df.Thrust .|> ustrip .|> uncertainty;\n\nair = DataFrame(Thrust=thrust_values, Uncertainty=thrust_uncertainties, Time=df.Time .|> u\"s\" .|> ustrip);\n\n\nplot(df.Time .|> ustrip, thrust_values,\n title=\"Thrust Over Time\",\n ribbon=(thrust_uncertainties, thrust_uncertainties),\n fillalpha=0.2, label=\"Thrust\",\n xlabel=\"Time (s)\",\n ylabel=\"Thrust (N)\",\n)\n```\n\n::: {.cell-output .cell-output-display execution_count=8}\n```{=html}\n \n \n```\n\nAir Propulsion Simulation\n:::\n:::\n\n\nIn Figure 2, the air propulsion simulation is compared to commercially available rocket motors. This early in the project, we have no idea whether short burns or longer burns are ideal for a propulsive landing, so the air propulsion system was compared to a variety of different motors with unique profiles. \n\n::: {#b7f911ae .cell code-folding='tru' execution_count=7}\n``` {.julia .cell-code}\nf10 = CSV.read(\"AeroTech_F10.csv\", DataFrame);\nf15 = CSV.read(\"Estes_F15.csv\", DataFrame);\ng8 = CSV.read(\"AeroTech_G8ST.csv\", DataFrame);\n\nplot(air.Time, air.Thrust, label=\"Air Propulsion\", legend=:outertopright);\n\nfor (d, l) in [(f10, \"F10\"), (f15, \"F15\"), (g8, \"G8ST\")]\n plot!(d[!, \"Time (s)\"], d[!, \"Thrust (N)\"], label=l)\nend\n\ntitle!(\"Propulsion Comparison\");\nxlabel!(\"Time (s)\");\nylabel!(\"Thrust (N)\")\n```\n\n::: {.cell-output .cell-output-display execution_count=9}\n```{=html}\n \n \n```\n\nRocket Motor Data: [@thrustcurve]\n:::\n:::\n\n\nIn the end, the air propulsion system's performance has a very impressive total impulse and, with more time and resources, could be a serious option for a propulsive landing on Earth. One of the largest abstractions from the Moon mission that the mission here on Earth will have to deal with is the lack of Throttling engines since any propulsion system outside of model rocket motors is well beyond the scope of this Capstone.\n\n## Future Work\n\nAfter determining that solid model rocket motors are the best option for the current mission scope, the next step is determining what motor to use. There are many great options, and deciding what thrust profile is ideal may have to wait until a Simulink simulation of the landing can be built so that the metrics of each motor can be constrained more. Instead of throttling motors, the current working idea is that thrust vector control may be a way to squeeze a little more control out of a solid rocket motor. Thrust Vector Control will undoubtedly be challenging to control, so another essential piece that needs exploring is whether an LQR controller is feasible or if a PID controller is accurate enough to control our system. \n\n\nCodebase: [https://gitlab.com/lander-team/air-prop-simulation](https://gitlab.com/lander-team/air-prop-simulation)\n\n",
+ "markdown": "---\ntitle: \"Air Propulsion Simulation\"\ndescription: |\n Simulating the performance of a compressed air propulsion system as an alternative to solid rocket motors using Julia.\ndescription-meta: |\n Simulate air propulsion for lunar mining transport! This project explores using compressed air as an alternative to solid rocket motors. See the Julia simulation results and comparisons with traditional rocket motor performance. Explore the code and learn more about this innovative approach.\nrepository_url: https://gitlab.com/lander-team/air-prop-simulation\ndate: 2021-04-01\ndate-modified: 2024-03-10\ncategories:\n - Julia\n - Capstone\n - University\n - Code\n - Aerospace\n - Math\ncreative_commons: CC BY\nbanner: prop_comp.png\nimage-alt: A line graph comparing the thrust of different propulsion systems over time. The x-axis represents time in seconds, and the y-axis represents thrust in Newtons. The graph displays the thrust curves for Air Propulsion, F10, F15, and G8ST.\nformat:\n html:\n code-tools: true\n code-fold: false\nexecute:\n output: false\nfreeze: true\n---\n\nFor Capstone my team was tasked with designing a system capable of moving mining equipment and materials around the surface of the Moon using a propulsive landing. The system had to be tested on Earth with something feasible for our team to build in 2 semesters. One of the first considerations my capstone advisor wanted was to test the feasibility of an air propulsion system instead of the obvious solution that of using solid rocket motors. This document is just _napkin math_ to determine if the system is even feasibly and is not meant to be a rigorous study of an air propulsion system that would easily keep a capstone team busy by itself. \n\n::: {#4b03dcc3 .cell execution_count=1}\n``` {.julia .cell-code code-fold=\"true\" code-summary=\"Imports\"}\nusing Plots\nplotlyjs()\n\nusing Unitful\nusing DataFrames\nusing Measurements\nusing Measurements: value, uncertainty\nusing CSV\n```\n:::\n\n\n## The Simulation\n\nI chose an off-the-shelf paintball gun tank for the pressure vessel. The primary consideration was the incredible pressure to weight ratio, and the fact that it is designed to be bumped around would be necessary for proving the safety of the system further into the project. \n\n::: {#3b0d3b37 .cell execution_count=2}\n``` {.julia .cell-code code-fold=\"false\"}\n# Tank https://www.amazon.com/Empire-Paintball-BASICS-Pressure-Compressed/dp/B07B6M48SR/\nV = (85 ± 5)u\"inch^3\";\nP0 = (4200.0 ± 300)u\"psi\";\nWtank = (2.3 ± 0.2)u\"lb\";\nPmax = (250 ± 50)u\"psi\"; # Max Pressure that can come out the nozzle\n```\n:::\n\n\nThe nozzle diameter was changed until the air prop system had a _burn time_ similar to a G18ST rocket motor. The propulsion system's total impulse is not dependant on the nozzle diameter, so this was just done to make it plot nicely with the rest of the rocket motors since, at this time, it is unknown what the optimal thrust profile is. \n\n::: {#fb49f3ca .cell execution_count=3}\n``` {.julia .cell-code}\n# Params\nd_nozzle = ((1 // 18) ± 0.001)u\"inch\";\na_nozzle = (pi / 4) * d_nozzle^2;\n```\n:::\n\n\nThese are just universal values for what a typical day would look like during the summer in Northern Arizona. [@cengel_thermodynamics]\n\n::: {#690060f1 .cell execution_count=4}\n``` {.julia .cell-code}\n# Universal Stuff\nP_amb = (1 ± 0.2)u\"atm\";\nγ = 1.4 ± 0.05;\nR = 287.05u\"J/(kg * K)\";\nT = (300 ± 20)u\"K\";\n```\n:::\n\n\nThe actual simulation is quite simple. The basic idea is that using the current pressure, you can calculate $\\dot{m}$, which allows calculating the Thrust, and then you can subtract the current mass of air in the tank by $\\dot{m}$ and recalculate pressure using the new mass then repeat the whole process.\n\n\nThe bulk of the equations in the simulation came from [@cengel_thermodynamics], while the Thrust and $v_e$ equations came from [@sutton_rocket_2001, eq: 2-14].\n\n$$ T = \\dot{m} \\cdot v_\\text{Exit} + A_\\text{Nozzle} \\cdot (P - P_\\text{Ambient}) $$\n\nThe initial pressure difference is 4190.0 ± 300.0 psi, which is massive, so the area of the nozzle significantly alters the thrust profile. The paintball tanks come with pressure regulators, in our case, 800 psi which is still a huge number compared to atmospheric pressure. While the total impulse of the system doesn't change with different nozzle areas, the peak thrust and _burn time_ vary greatly. One of the benefits of doing air propulsion and the reason it was even considered so seriously is that it should be possible to change the nozzle diameter in flight, allowing thrust to be throttled, making controlled landing easier to control. \n\n::: {#f812ad33 .cell execution_count=5}\n``` {.julia .cell-code}\nt = 0.0u\"s\";\nP = P0 |> u\"Pa\";\nM = V * (P / (R * T)) |> u\"kg\";\nts = 1u\"ms\";\ndf = DataFrame(Thrust=(0 ± 0)u\"N\", Pressure=P0, Time=0.0u\"s\", Mass=M);\nwhile M > 0.005u\"kg\"\n # Calculate what is leaving tank\n P = minimum([P, Pmax])\n ve = sqrt((2 * γ / (γ - 1)) * R * T * (1 - P_amb / P)^((γ - 1) / γ)) |> u\"m/s\"\n ρ = P / (R * T) |> u\"kg/m^3\"\n ṁ = ρ * a_nozzle * ve |> u\"kg/s\"\n\n Thrust = ṁ * ve + a_nozzle * (P - P_amb) |> u\"N\"\n\n # Calculate what is still in the tank\n M = M - ṁ * ts |> u\"kg\"\n P = (M * R * T) / V |> u\"Pa\"\n t = t + ts\n\n df_step = DataFrame(Thrust=Thrust, Pressure=P, Time=t, Mass=M)\n append!(df, df_step)\nend\n```\n:::\n\n\n## Analysis\n\n\nBelow in figure 1, the result of the simulation is plotted. Notice the massive error once the tank starts running low. This is because the calculation for pressure has a lot of very uncertain variables. This is primarily due to air being a compressible fluid, making this simulation challenging to do accurately. The thrust being below 0 N might not make intuitive sense, but it's technically possible for the pressure to compress, leaving the inside of the rocket nozzle with a pressure that's actually below atmospheric pressure. The effect would likely last a fraction of a second, but the point stands that this simulation is wildly inaccurate and only meant to get an idea of what an air propulsion system is capable of. \n\n::: {#3ed8adc4 .cell class-output='preview-image' code-folding='true' execution_count=6}\n``` {.julia .cell-code}\nthrust_values = df.Thrust .|> ustrip .|> value;\nthrust_uncertainties = df.Thrust .|> ustrip .|> uncertainty;\ntime_values = df.Time .|> ustrip;\n\n# Downsample for plotting (keep ~1000 points for visual fidelity)\nn_points = length(thrust_values)\nstep = max(1, n_points ÷ 1000)\nidx = 1:step:n_points\n\nair = DataFrame(\n Thrust=thrust_values[idx],\n Uncertainty=thrust_uncertainties[idx],\n Time=time_values[idx]\n);\n\nplot(air.Time, air.Thrust,\n title=\"Thrust Over Time\",\n ribbon=(air.Uncertainty, air.Uncertainty),\n fillalpha=0.2, label=\"Thrust\",\n xlabel=\"Time (s)\",\n ylabel=\"Thrust (N)\",\n)\n```\n\n::: {.cell-output .cell-output-display execution_count=8}\n```{=html}\n \n \n```\n\nAir Propulsion Simulation\n:::\n:::\n\n\nIn Figure 2, the air propulsion simulation is compared to commercially available rocket motors. This early in the project, we have no idea whether short burns or longer burns are ideal for a propulsive landing, so the air propulsion system was compared to a variety of different motors with unique profiles. \n\n::: {#cc00dad6 .cell code-folding='tru' execution_count=7}\n``` {.julia .cell-code}\nf10 = CSV.read(\"AeroTech_F10.csv\", DataFrame);\nf15 = CSV.read(\"Estes_F15.csv\", DataFrame);\ng8 = CSV.read(\"AeroTech_G8ST.csv\", DataFrame);\n\nplot(air.Time, air.Thrust, label=\"Air Propulsion\", legend=:outertopright);\n\nfor (d, l) in [(f10, \"F10\"), (f15, \"F15\"), (g8, \"G8ST\")]\n plot!(d[!, \"Time (s)\"], d[!, \"Thrust (N)\"], label=l)\nend\n\ntitle!(\"Propulsion Comparison\");\nxlabel!(\"Time (s)\");\nylabel!(\"Thrust (N)\")\n```\n\n::: {.cell-output .cell-output-display execution_count=9}\n```{=html}\n \n \n```\n\nRocket Motor Data: [@thrustcurve]\n:::\n:::\n\n\nIn the end, the air propulsion system's performance has a very impressive total impulse and, with more time and resources, could be a serious option for a propulsive landing on Earth. One of the largest abstractions from the Moon mission that the mission here on Earth will have to deal with is the lack of Throttling engines since any propulsion system outside of model rocket motors is well beyond the scope of this Capstone.\n\n## Future Work\n\nAfter determining that solid model rocket motors are the best option for the current mission scope, the next step is determining what motor to use. There are many great options, and deciding what thrust profile is ideal may have to wait until a Simulink simulation of the landing can be built so that the metrics of each motor can be constrained more. Instead of throttling motors, the current working idea is that thrust vector control may be a way to squeeze a little more control out of a solid rocket motor. Thrust Vector Control will undoubtedly be challenging to control, so another essential piece that needs exploring is whether an LQR controller is feasible or if a PID controller is accurate enough to control our system. \n\n\nCodebase: [https://gitlab.com/lander-team/air-prop-simulation](https://gitlab.com/lander-team/air-prop-simulation)\n\n",
"supporting": [
"index_files"
],
diff --git a/Anson-Projects/projects/.quarto/_freeze/posts/2021-04-14-iss-eclipse-determination/index/execute-results/html.json b/Anson-Projects/projects/.quarto/_freeze/posts/2021-04-14-iss-eclipse-determination/index/execute-results/html.json
index d1e28ccda..8526dbe8d 100644
--- a/Anson-Projects/projects/.quarto/_freeze/posts/2021-04-14-iss-eclipse-determination/index/execute-results/html.json
+++ b/Anson-Projects/projects/.quarto/_freeze/posts/2021-04-14-iss-eclipse-determination/index/execute-results/html.json
@@ -1,8 +1,8 @@
{
- "hash": "89ff49a6b9e2d75bb7f69c6ed2418a3a",
+ "hash": "aa1423020fa8b85658b268c0cbdd9cf2",
"result": {
"engine": "jupyter",
- "markdown": "---\ntitle: \"ISS Eclipse Determination\"\ndescription: |\n Calculate sunlight exposure for orbiting spacecraft like the ISS. This Julia project demonstrates how to determine eclipse times, considering umbra, penumbra, and full sunlight. Visualizations and code included. Learn about orbital mechanics and mission design considerations.\ndate: 2021-05-01\ndate-modified: 2024-02-292\ncategories:\n - Julia\n - Astrodynamics\n - Code\n - Aerospace\n - Notes\n - Space\n - Math\ncreative_commons: CC BY\nimage: preview.png\nimage-alt: A diagram illustrating the umbra and penumbra regions cast by a celestial body.\nformat:\n html:\n code-tools: true\n code-fold: false\nfreeze: true\n---\n\n\n\n\nDetermining the eclipses a satellite will encounter is a major driving factor when designing a mission in space. Thermal and power budgets have to be made with the fact that a satellite will periodically be in the complete darkness of space where it will receive no solar radiation to power the solar panels and keep the spacecraft from freezing.\n\n## What is an Eclipse\n\n{fig-alt=\"A diagram of an eclipse. The sun is shown as a large yellow circle on the left. A smaller blue circle labeled \"Body\" is to the right of the sun. A spacecraft is shown above the body. The umbra and penumbra are labeled.\"}\n\nThe above image is a simple representation of what an eclipse is. First, you'll notice the Umbra is complete darkness, then the Penumbra, which is a shadow of varying darkness, and then the rest of the orbit is in full sunlight. For this example, I will be using the ISS, which has a very low orbit, so the Penumbra isn't much of a problem. However, you can tell by looking at the diagram that higher altitude orbits would spend more time in the Penumbra. \n\n{fig-alt=\"A diagram expanding on the last figure, but with distances marked for the radii of the sun and body, and the distance between the body and spacecraft.\"}\n\nHere is a more detailed view of the eclipse that will make it easier to explain what is going on. There are 2 Position vectors, and 2 radius that need to be known for simple eclipse determination. More advanced cases where the atmosphere of the body your orbiting can significantly affect the Umbra and Penumbra, and other bodies could also potentially block the Sun. However, we will keep it simple for this example since they have minimal effect on the ISS’s orbit. Rsun and Rbody are the radius of the Sun and Body (In this case Earth), respectively. r_sun_body is a vector from the center of the Sun to the center of the target body. For this example I will only be using one vector, but for more rigorous eclipse determination it is important to calculate the ephemeris at least once a day since it does significantly change over the course of a year. The reason that I am ignoring it at the moment is because there is currently no good way to calculate [Ephemerides](https://ssd.jpl.nasa.gov/?ephemerides) in Julia but the package is being worked on so I may revisit this and do a more rigorous analysis in the future. r_body_sc is a position vector from the center of the body being orbited, to the center of our spacecraft. \n\n## The Code\n\n::: {#f15ebfa7 .cell execution_count=1}\n``` {.julia .cell-code code-fold=\"true\" code-summary=\"Imports\"}\nusing Unitful\nusing LinearAlgebra\nusing SatelliteToolbox\nusing Plots\nusing Colors\ntheme(:ggplot2)\n```\n:::\n\n\nTo get the orbit for the ISS, I used a [Two-Line Element](https://en.wikipedia.org/wiki/Two-line_element_set) which is a data format for explaining orbits. The US Joint Space Operations Center makes these widely available, but https://live.ariss.org/tle/ makes the TLE for the ISS way more accessible [@ariss]. The Julia Package [SatelliteToolbox.jl](https://github.com/JuliaSpace/SatelliteToolbox.jl) makes it super easy to turn a TLE into an orbit that can be propagated. Simply putting the TLE in a string and using the `tle` string macro like below, we now have access to the information to start making our ISS orbit. \n\n::: {#788e2079 .cell execution_count=2}\n``` {.julia .cell-code}\nISS = tle\"\"\"\nISS (ZARYA)\n1 25544U 98067A 21103.84943184 .00000176 00000-0 11381-4 0 9990\n2 25544 51.6434 300.9481 0002858 223.8443 263.8789 15.48881793278621\n\"\"\"\n```\n\n::: {.cell-output .cell-output-display execution_count=3}\n\n::: {.ansi-escaped-output}\n```{=html}\n
TLE:\n Name : ISS (ZARYA)\n Satellite number : 25544\n International designator : 98067A\n Epoch (Year / Day) : 21 / 103.84943184 (2021-04-13T20:23:10.911)\n Element set number : 999\n Eccentricity : 0.00028580\n Inclination : 51.64340000 deg\n RAAN : 300.94810000 deg\n Argument of perigee : 223.84430000 deg\n Mean anomaly : 263.87890000 deg\n Mean motion (n) : 15.48881793 revs / day\n Revolution number : 27862\n B* : 1.1381e-05 1 / er\n ṅ / 2 : 1.76e-06 rev / day²\n n̈ / 6 : 0 rev / day³
\n```\n:::\n\n:::\n:::\n\n\nNow that we have the TLE, we can pass that into SatelliteToolbox's orbit propagator. Before propagating the orbit, we need to have a range of time steps to pass into the propagator. The TLE gives the mean motion, n, which is the revolutions per day, so using that, we can calculate the amount of time required for one orbit, which is all that we're worried about for this analysis. The propagator returns a tuple containing the Orbital elements, a position vector with units meters, and a velocity vector with units meters per second. For this analysis were only worried about the position vector. \n\n::: {#7272b34c .cell execution_count=3}\n``` {.julia .cell-code}\nISS.mean_motion\n```\n\n::: {.cell-output .cell-output-display execution_count=4}\n```\n15.48881793\n```\n:::\n:::\n\n\n::: {#c16df9f1 .cell execution_count=4}\n``` {.julia .cell-code}\norbit = Propagators.init(Val(:SGP4), ISS);\ntime = 0:0.1:((24/ISS.mean_motion).*60*60);\npropagated = Propagators.propagate!.(orbit, time);\nr = first.(propagated); # Get distance from propagator\n```\n:::\n\n\nWe just need to use the radii and vectors discussed earlier to determine if the ISS is in the penumbra or umbra. This is a lot of trigonometry and vector math that I won't bore anyone with. However, using the diagrams above and following the code in the sunlight function, you should follow what's happening. For a rigorous discussion, check out [@vallado]. \n\n::: {#7c9b979b .cell execution_count=5}\n``` {.julia .cell-code}\nfunction sunlight(Rbody, r_sun_body, r_body_sc)\n Rsun = 695_700u\"km\"\n\n hu = Rbody * norm(r_sun_body) / (Rsun - Rbody)\n\n θe = acos((r_sun_body ⋅ r_body_sc) / (norm(r_sun_body) * norm(r_body_sc)))\n\n θu = atan(Rbody / hu)\n du = hu * sin(θu) / sin(θe + θu)\n\n θp = π - atan(norm(r_sun_body) / (Rsun + Rbody))\n dp = Rbody * sin(θp) / cos(θe - θp)\n\n S = 1\n if (θe < π / 2) && (norm(r_body_sc) < du)\n S = 0\n end\n if (θe < π / 2) && ((du < norm(r_body_sc)) && (norm(r_body_sc) < dp))\n S = (norm(r_body_sc .|> u\"km\") - du) / (dp - du) |> ustrip\n end\n\n return S\nend\n```\n\n::: {.cell-output .cell-output-display execution_count=6}\n```\nsunlight (generic function with 1 method)\n```\n:::\n:::\n\n\nThen we can pass all the values we've gathered into the function we just made. \n\n::: {#efb9e6d8 .cell execution_count=6}\n``` {.julia .cell-code}\nS = r .|> R -> sunlight(6371u\"km\", [0.5370, 1.2606, 0.5466] .* 1e8u\"km\", R .* u\"m\");\n```\n:::\n\n\n## Plotting the Results\n\nThe `sunlight` function returns values from 0 to 1, 0 being complete darkness, 1 being complete sunlight, and anything between being the fraction of light being received. Again since the ISS has a very low orbit, the amount of time spent in the penumbra is almost insignificant. \n\n::: {#28ea52a5 .cell alt-text='A graph titled \"ISS Sunlight Over a Day\" showing the percentage of sunlight the ISS receives over a 24-hour period. The x-axis represents time in hours, and the y-axis represents sunlight percentage. The graph shows a period of near-total sunlight followed by a period of darkness, and the cycle repeats.' execution_count=7}\n``` {.julia .cell-code code-fold=\"true\"}\n# Get fancy with the line color. \nlight_range = range(colorant\"black\", stop=colorant\"orange\", length=101);\nlight_colors = [light_range[unique(round(Int, 1 + s * 100))][1] for s in S];\n\nplot(\n LinRange(0, 24, length(S)),\n S .* 100,\n linewidth=5,\n legend=false,\n color=light_colors,\n);\n\nxlabel!(\"Time (hr)\");\nylabel!(\"Sunlight (%)\");\ntitle!(\"ISS Sunlight Over a Day\")\n```\n\n::: {.cell-output .cell-output-display execution_count=8}\n```{=html}\n\n\n```\n\nISS Sunlight\n:::\n:::\n\n\nLooking at the plot, the vertical transition from 0% to 100% makes it pretty clear that the time in the penumbra is limited. Still, almost counterintuitively, it also looks like the ISS gets more sunlight than it does darkness. So, using the raw sunlight data, we can calculate precisely how much time is spent in each region. \n\nTime in Sun:\n\n::: {#f993b102 .cell execution_count=8}\n``` {.julia .cell-code}\nsun = length(S[S.==1]) / length(S) * 100\n```\n\n::: {.cell-output .cell-output-display execution_count=9}\n```\n62.04757721886597\n```\n:::\n:::\n\n\nTime in Darkness:\n\n::: {#ab190695 .cell execution_count=9}\n``` {.julia .cell-code}\numbra = length(S[S.==0]) / length(S) * 100\n```\n\n::: {.cell-output .cell-output-display execution_count=10}\n```\n37.627951167918546\n```\n:::\n:::\n\n\nTime in Penumbra:\n\n::: {#bf25b850 .cell execution_count=10}\n``` {.julia .cell-code}\npenumbra = 100 - umbra - sun\n```\n\n::: {.cell-output .cell-output-display execution_count=11}\n```\n0.32447161321548634\n```\n:::\n:::\n\n\nThis means that even with the ISS's low orbit, it still gets sunlight ~62% of the time and spends almost no time in the penumbra. This would vary a few percent depending on the time of year, but in a circular orbit like the ISS, the amount of sunlight would remain pretty constant. There are other orbits like a polar orbit, lunar orbit, or highly elliptic earth orbits that can have their time in the sunlight vary widely by the time of year. \n\n",
+ "markdown": "---\ntitle: \"ISS Eclipse Determination\"\ndescription: |\n Calculate sunlight exposure for orbiting spacecraft like the ISS. This Julia project demonstrates how to determine eclipse times, considering umbra, penumbra, and full sunlight. Visualizations and code included. Learn about orbital mechanics and mission design considerations.\ndate: 2021-05-01\ndate-modified: 2024-02-292\ncategories:\n - Julia\n - Astrodynamics\n - Code\n - Aerospace\n - Notes\n - Space\n - Math\ncreative_commons: CC BY\nimage: preview.png\nimage-alt: A diagram illustrating the umbra and penumbra regions cast by a celestial body.\nformat:\n html:\n code-tools: true\n code-fold: false\nfreeze: true\n---\n\nDetermining the eclipses a satellite will encounter is a major driving factor when designing a mission in space. Thermal and power budgets have to be made with the fact that a satellite will periodically be in the complete darkness of space where it will receive no solar radiation to power the solar panels and keep the spacecraft from freezing.\n\n## What is an Eclipse\n\n{fig-alt=\"A diagram of an eclipse. The sun is shown as a large yellow circle on the left. A smaller blue circle labeled \"Body\" is to the right of the sun. A spacecraft is shown above the body. The umbra and penumbra are labeled.\"}\n\nThe above image is a simple representation of what an eclipse is. First, you'll notice the Umbra is complete darkness, then the Penumbra, which is a shadow of varying darkness, and then the rest of the orbit is in full sunlight. For this example, I will be using the ISS, which has a very low orbit, so the Penumbra isn't much of a problem. However, you can tell by looking at the diagram that higher altitude orbits would spend more time in the Penumbra. \n\n{fig-alt=\"A diagram expanding on the last figure, but with distances marked for the radii of the sun and body, and the distance between the body and spacecraft.\"}\n\nHere is a more detailed view of the eclipse that will make it easier to explain what is going on. There are 2 Position vectors, and 2 radius that need to be known for simple eclipse determination. More advanced cases where the atmosphere of the body your orbiting can significantly affect the Umbra and Penumbra, and other bodies could also potentially block the Sun. However, we will keep it simple for this example since they have minimal effect on the ISS’s orbit. Rsun and Rbody are the radius of the Sun and Body (In this case Earth), respectively. r_sun_body is a vector from the center of the Sun to the center of the target body. For this example I will only be using one vector, but for more rigorous eclipse determination it is important to calculate the ephemeris at least once a day since it does significantly change over the course of a year. The reason that I am ignoring it at the moment is because there is currently no good way to calculate [Ephemerides](https://ssd.jpl.nasa.gov/?ephemerides) in Julia but the package is being worked on so I may revisit this and do a more rigorous analysis in the future. r_body_sc is a position vector from the center of the body being orbited, to the center of our spacecraft. \n\n## The Code\n\n::: {#f92c4c22 .cell execution_count=1}\n``` {.julia .cell-code code-fold=\"true\" code-summary=\"Imports\"}\nusing Unitful\nusing LinearAlgebra\nusing SatelliteToolbox\nusing Plots\nusing Colors\ntheme(:ggplot2)\n```\n:::\n\n\nTo get the orbit for the ISS, I used a [Two-Line Element](https://en.wikipedia.org/wiki/Two-line_element_set) which is a data format for explaining orbits. The US Joint Space Operations Center makes these widely available, but https://live.ariss.org/tle/ makes the TLE for the ISS way more accessible [@ariss]. The Julia Package [SatelliteToolbox.jl](https://github.com/JuliaSpace/SatelliteToolbox.jl) makes it super easy to turn a TLE into an orbit that can be propagated. Simply putting the TLE in a string and using the `tle` string macro like below, we now have access to the information to start making our ISS orbit. \n\n::: {#d8a9cdaf .cell execution_count=2}\n``` {.julia .cell-code}\nISS = tle\"\"\"\nISS (ZARYA)\n1 25544U 98067A 21103.84943184 .00000176 00000-0 11381-4 0 9990\n2 25544 51.6434 300.9481 0002858 223.8443 263.8789 15.48881793278621\n\"\"\"\n```\n\n::: {.cell-output .cell-output-display execution_count=3}\n\n::: {.ansi-escaped-output}\n```{=html}\n
TLE:\n Name : ISS (ZARYA)\n Satellite number : 25544\n International designator : 98067A\n Epoch (Year / Day) : 21 / 103.84943184 (2021-04-13T20:23:10.911)\n Element set number : 999\n Eccentricity : 0.00028580\n Inclination : 51.64340000 deg\n RAAN : 300.94810000 deg\n Argument of perigee : 223.84430000 deg\n Mean anomaly : 263.87890000 deg\n Mean motion (n) : 15.48881793 revs / day\n Revolution number : 27862\n B* : 1.1381e-05 1 / er\n ṅ / 2 : 1.76e-06 rev / day²\n n̈ / 6 : 0 rev / day³
\n```\n:::\n\n:::\n:::\n\n\nNow that we have the TLE, we can pass that into SatelliteToolbox's orbit propagator. Before propagating the orbit, we need to have a range of time steps to pass into the propagator. The TLE gives the mean motion, n, which is the revolutions per day, so using that, we can calculate the amount of time required for one orbit, which is all that we're worried about for this analysis. The propagator returns a tuple containing the Orbital elements, a position vector with units meters, and a velocity vector with units meters per second. For this analysis were only worried about the position vector. \n\n::: {#b25e8aec .cell execution_count=3}\n``` {.julia .cell-code}\nISS.mean_motion\n```\n\n::: {.cell-output .cell-output-display execution_count=4}\n```\n15.48881793\n```\n:::\n:::\n\n\n::: {#a3946215 .cell execution_count=4}\n``` {.julia .cell-code}\norbit = Propagators.init(Val(:SGP4), ISS);\ntime = 0:0.1:((24/ISS.mean_motion).*60*60);\npropagated = Propagators.propagate!.(orbit, time);\nr = first.(propagated); # Get distance from propagator\n```\n:::\n\n\nWe just need to use the radii and vectors discussed earlier to determine if the ISS is in the penumbra or umbra. This is a lot of trigonometry and vector math that I won't bore anyone with. However, using the diagrams above and following the code in the sunlight function, you should follow what's happening. For a rigorous discussion, check out [@vallado]. \n\n::: {#6646946f .cell execution_count=5}\n``` {.julia .cell-code}\nfunction sunlight(Rbody, r_sun_body, r_body_sc)\n Rsun = 695_700u\"km\"\n\n hu = Rbody * norm(r_sun_body) / (Rsun - Rbody)\n\n θe = acos((r_sun_body ⋅ r_body_sc) / (norm(r_sun_body) * norm(r_body_sc)))\n\n θu = atan(Rbody / hu)\n du = hu * sin(θu) / sin(θe + θu)\n\n θp = π - atan(norm(r_sun_body) / (Rsun + Rbody))\n dp = Rbody * sin(θp) / cos(θe - θp)\n\n S = 1\n if (θe < π / 2) && (norm(r_body_sc) < du)\n S = 0\n end\n if (θe < π / 2) && ((du < norm(r_body_sc)) && (norm(r_body_sc) < dp))\n S = (norm(r_body_sc .|> u\"km\") - du) / (dp - du) |> ustrip\n end\n\n return S\nend\n```\n\n::: {.cell-output .cell-output-display execution_count=6}\n```\nsunlight (generic function with 1 method)\n```\n:::\n:::\n\n\nThen we can pass all the values we've gathered into the function we just made. \n\n::: {#ac28930b .cell execution_count=6}\n``` {.julia .cell-code}\nS = r .|> R -> sunlight(6371u\"km\", [0.5370, 1.2606, 0.5466] .* 1e8u\"km\", R .* u\"m\");\n```\n:::\n\n\n## Plotting the Results\n\nThe `sunlight` function returns values from 0 to 1, 0 being complete darkness, 1 being complete sunlight, and anything between being the fraction of light being received. Again since the ISS has a very low orbit, the amount of time spent in the penumbra is almost insignificant. \n\n::: {#7069bc07 .cell alt-text='A graph titled \"ISS Sunlight Over a Day\" showing the percentage of sunlight the ISS receives over a 24-hour period. The x-axis represents time in hours, and the y-axis represents sunlight percentage. The graph shows a period of near-total sunlight followed by a period of darkness, and the cycle repeats.' execution_count=7}\n``` {.julia .cell-code code-fold=\"true\"}\n# Downsample for plotting (keep ~1000 points for visual fidelity)\nn_points = length(S)\nstep = max(1, n_points ÷ 1000)\nidx = 1:step:n_points\nS_plot = S[idx]\n\n# Get fancy with the line color.\nlight_range = range(colorant\"black\", stop=colorant\"orange\", length=101);\nlight_colors = [light_range[unique(round(Int, 1 + s * 100))][1] for s in S_plot];\n\nplot(\n LinRange(0, 24, length(S_plot)),\n S_plot .* 100,\n linewidth=5,\n legend=false,\n color=light_colors,\n);\n\nxlabel!(\"Time (hr)\");\nylabel!(\"Sunlight (%)\");\ntitle!(\"ISS Sunlight Over a Day\")\n```\n\n::: {.cell-output .cell-output-display execution_count=8}\n```{=html}\n\n```\n\nISS Sunlight\n:::\n:::\n\n\nLooking at the plot, the vertical transition from 0% to 100% makes it pretty clear that the time in the penumbra is limited. Still, almost counterintuitively, it also looks like the ISS gets more sunlight than it does darkness. So, using the raw sunlight data, we can calculate precisely how much time is spent in each region. \n\nTime in Sun:\n\n::: {#f7fe5f20 .cell execution_count=8}\n``` {.julia .cell-code}\nsun = length(S[S.==1]) / length(S) * 100\n```\n\n::: {.cell-output .cell-output-display execution_count=9}\n```\n62.04757721886597\n```\n:::\n:::\n\n\nTime in Darkness:\n\n::: {#051e5dc2 .cell execution_count=9}\n``` {.julia .cell-code}\numbra = length(S[S.==0]) / length(S) * 100\n```\n\n::: {.cell-output .cell-output-display execution_count=10}\n```\n37.627951167918546\n```\n:::\n:::\n\n\nTime in Penumbra:\n\n::: {#125c4904 .cell execution_count=10}\n``` {.julia .cell-code}\npenumbra = 100 - umbra - sun\n```\n\n::: {.cell-output .cell-output-display execution_count=11}\n```\n0.32447161321548634\n```\n:::\n:::\n\n\nThis means that even with the ISS's low orbit, it still gets sunlight ~62% of the time and spends almost no time in the penumbra. This would vary a few percent depending on the time of year, but in a circular orbit like the ISS, the amount of sunlight would remain pretty constant. There are other orbits like a polar orbit, lunar orbit, or highly elliptic earth orbits that can have their time in the sunlight vary widely by the time of year. \n\n",
"supporting": [
"index_files"
],
diff --git a/Anson-Projects/projects/.quarto/_freeze/site_libs/quarto-listing/quarto-listing.js b/Anson-Projects/projects/.quarto/_freeze/site_libs/quarto-listing/quarto-listing.js
index e9a07b2ea..54d0e1e7f 100644
--- a/Anson-Projects/projects/.quarto/_freeze/site_libs/quarto-listing/quarto-listing.js
+++ b/Anson-Projects/projects/.quarto/_freeze/site_libs/quarto-listing/quarto-listing.js
@@ -2,8 +2,7 @@ const kProgressiveAttr = "data-src";
let categoriesLoaded = false;
window.quartoListingCategory = (category) => {
- // category is URI encoded in EJS template for UTF-8 support
- category = decodeURIComponent(atob(category));
+ category = atob(category);
if (categoriesLoaded) {
activateCategory(category);
setCategoryHash(category);
diff --git a/Anson-Projects/projects/_freeze/posts/2021-04-01-air-propulsion-simulation/index/execute-results/html.json b/Anson-Projects/projects/_freeze/posts/2021-04-01-air-propulsion-simulation/index/execute-results/html.json
index 63144dc90..905e1c35e 100644
--- a/Anson-Projects/projects/_freeze/posts/2021-04-01-air-propulsion-simulation/index/execute-results/html.json
+++ b/Anson-Projects/projects/_freeze/posts/2021-04-01-air-propulsion-simulation/index/execute-results/html.json
@@ -1,8 +1,8 @@
{
- "hash": "f949d3b331ebc31b94d98d3f77365500",
+ "hash": "e86f7f84efe6d892be0f803d1a118411",
"result": {
"engine": "jupyter",
- "markdown": "---\ntitle: \"Air Propulsion Simulation\"\ndescription: |\n Simulating the performance of a compressed air propulsion system as an alternative to solid rocket motors using Julia.\ndescription-meta: |\n Simulate air propulsion for lunar mining transport! This project explores using compressed air as an alternative to solid rocket motors. See the Julia simulation results and comparisons with traditional rocket motor performance. Explore the code and learn more about this innovative approach.\nrepository_url: https://gitlab.com/lander-team/air-prop-simulation\ndate: 2021-04-01\ndate-modified: 2024-03-10\ncategories:\n - Julia\n - Capstone\n - University\n - Code\n - Aerospace\n - Math\ncreative_commons: CC BY\nbanner: prop_comp.png\nimage-alt: A line graph comparing the thrust of different propulsion systems over time. The x-axis represents time in seconds, and the y-axis represents thrust in Newtons. The graph displays the thrust curves for Air Propulsion, F10, F15, and G8ST.\nformat:\n html:\n code-tools: true\n code-fold: false\nexecute:\n output: false\nfreeze: true\n---\n\n\n\n\nFor Capstone my team was tasked with designing a system capable of moving mining equipment and materials around the surface of the Moon using a propulsive landing. The system had to be tested on Earth with something feasible for our team to build in 2 semesters. One of the first considerations my capstone advisor wanted was to test the feasibility of an air propulsion system instead of the obvious solution that of using solid rocket motors. This document is just _napkin math_ to determine if the system is even feasibly and is not meant to be a rigorous study of an air propulsion system that would easily keep a capstone team busy by itself. \n\n::: {#074b7a47 .cell execution_count=1}\n``` {.julia .cell-code code-fold=\"true\" code-summary=\"Imports\"}\nusing Plots\nplotlyjs()\n\nusing Unitful\nusing DataFrames\nusing Measurements\nusing Measurements: value, uncertainty\nusing CSV\n```\n:::\n\n\n## The Simulation\n\nI chose an off-the-shelf paintball gun tank for the pressure vessel. The primary consideration was the incredible pressure to weight ratio, and the fact that it is designed to be bumped around would be necessary for proving the safety of the system further into the project. \n\n::: {#f11e6b2b .cell execution_count=2}\n``` {.julia .cell-code code-fold=\"false\"}\n# Tank https://www.amazon.com/Empire-Paintball-BASICS-Pressure-Compressed/dp/B07B6M48SR/\nV = (85 ± 5)u\"inch^3\";\nP0 = (4200.0 ± 300)u\"psi\";\nWtank = (2.3 ± 0.2)u\"lb\";\nPmax = (250 ± 50)u\"psi\"; # Max Pressure that can come out the nozzle\n```\n:::\n\n\nThe nozzle diameter was changed until the air prop system had a _burn time_ similar to a G18ST rocket motor. The propulsion system's total impulse is not dependant on the nozzle diameter, so this was just done to make it plot nicely with the rest of the rocket motors since, at this time, it is unknown what the optimal thrust profile is. \n\n::: {#fc20ecb1 .cell execution_count=3}\n``` {.julia .cell-code}\n# Params\nd_nozzle = ((1 // 18) ± 0.001)u\"inch\";\na_nozzle = (pi / 4) * d_nozzle^2;\n```\n:::\n\n\nThese are just universal values for what a typical day would look like during the summer in Northern Arizona. [@cengel_thermodynamics]\n\n::: {#df40fdff .cell execution_count=4}\n``` {.julia .cell-code}\n# Universal Stuff\nP_amb = (1 ± 0.2)u\"atm\";\nγ = 1.4 ± 0.05;\nR = 287.05u\"J/(kg * K)\";\nT = (300 ± 20)u\"K\";\n```\n:::\n\n\nThe actual simulation is quite simple. The basic idea is that using the current pressure, you can calculate $\\dot{m}$, which allows calculating the Thrust, and then you can subtract the current mass of air in the tank by $\\dot{m}$ and recalculate pressure using the new mass then repeat the whole process.\n\n\nThe bulk of the equations in the simulation came from [@cengel_thermodynamics], while the Thrust and $v_e$ equations came from [@sutton_rocket_2001, eq: 2-14].\n\n$$ T = \\dot{m} \\cdot v_\\text{Exit} + A_\\text{Nozzle} \\cdot (P - P_\\text{Ambient}) $$\n\nThe initial pressure difference is 4190.0 ± 300.0 psi, which is massive, so the area of the nozzle significantly alters the thrust profile. The paintball tanks come with pressure regulators, in our case, 800 psi which is still a huge number compared to atmospheric pressure. While the total impulse of the system doesn't change with different nozzle areas, the peak thrust and _burn time_ vary greatly. One of the benefits of doing air propulsion and the reason it was even considered so seriously is that it should be possible to change the nozzle diameter in flight, allowing thrust to be throttled, making controlled landing easier to control. \n\n::: {#062b34c8 .cell execution_count=5}\n``` {.julia .cell-code}\nt = 0.0u\"s\";\nP = P0 |> u\"Pa\";\nM = V * (P / (R * T)) |> u\"kg\";\nts = 1u\"ms\";\ndf = DataFrame(Thrust=(0 ± 0)u\"N\", Pressure=P0, Time=0.0u\"s\", Mass=M);\nwhile M > 0.005u\"kg\"\n # Calculate what is leaving tank\n P = minimum([P, Pmax])\n ve = sqrt((2 * γ / (γ - 1)) * R * T * (1 - P_amb / P)^((γ - 1) / γ)) |> u\"m/s\"\n ρ = P / (R * T) |> u\"kg/m^3\"\n ṁ = ρ * a_nozzle * ve |> u\"kg/s\"\n\n Thrust = ṁ * ve + a_nozzle * (P - P_amb) |> u\"N\"\n\n # Calculate what is still in the tank\n M = M - ṁ * ts |> u\"kg\"\n P = (M * R * T) / V |> u\"Pa\"\n t = t + ts\n\n df_step = DataFrame(Thrust=Thrust, Pressure=P, Time=t, Mass=M)\n append!(df, df_step)\nend\n```\n:::\n\n\n## Analysis\n\n\nBelow in figure 1, the result of the simulation is plotted. Notice the massive error once the tank starts running low. This is because the calculation for pressure has a lot of very uncertain variables. This is primarily due to air being a compressible fluid, making this simulation challenging to do accurately. The thrust being below 0 N might not make intuitive sense, but it's technically possible for the pressure to compress, leaving the inside of the rocket nozzle with a pressure that's actually below atmospheric pressure. The effect would likely last a fraction of a second, but the point stands that this simulation is wildly inaccurate and only meant to get an idea of what an air propulsion system is capable of. \n\n::: {#089fdc15 .cell class-output='preview-image' code-folding='true' execution_count=6}\n``` {.julia .cell-code}\nthrust_values = df.Thrust .|> ustrip .|> value;\nthrust_uncertainties = df.Thrust .|> ustrip .|> uncertainty;\n\nair = DataFrame(Thrust=thrust_values, Uncertainty=thrust_uncertainties, Time=df.Time .|> u\"s\" .|> ustrip);\n\n\nplot(df.Time .|> ustrip, thrust_values,\n title=\"Thrust Over Time\",\n ribbon=(thrust_uncertainties, thrust_uncertainties),\n fillalpha=0.2, label=\"Thrust\",\n xlabel=\"Time (s)\",\n ylabel=\"Thrust (N)\",\n)\n```\n\n::: {.cell-output .cell-output-display execution_count=8}\n```{=html}\n \n \n```\n\nAir Propulsion Simulation\n:::\n:::\n\n\nIn Figure 2, the air propulsion simulation is compared to commercially available rocket motors. This early in the project, we have no idea whether short burns or longer burns are ideal for a propulsive landing, so the air propulsion system was compared to a variety of different motors with unique profiles. \n\n::: {#13780fcc .cell code-folding='tru' execution_count=7}\n``` {.julia .cell-code}\nf10 = CSV.read(\"AeroTech_F10.csv\", DataFrame);\nf15 = CSV.read(\"Estes_F15.csv\", DataFrame);\ng8 = CSV.read(\"AeroTech_G8ST.csv\", DataFrame);\n\nplot(air.Time, air.Thrust, label=\"Air Propulsion\", legend=:outertopright);\n\nfor (d, l) in [(f10, \"F10\"), (f15, \"F15\"), (g8, \"G8ST\")]\n plot!(d[!, \"Time (s)\"], d[!, \"Thrust (N)\"], label=l)\nend\n\ntitle!(\"Propulsion Comparison\");\nxlabel!(\"Time (s)\");\nylabel!(\"Thrust (N)\")\n```\n\n::: {.cell-output .cell-output-display execution_count=9}\n```{=html}\n \n \n```\n\nRocket Motor Data: [@thrustcurve]\n:::\n:::\n\n\nIn the end, the air propulsion system's performance has a very impressive total impulse and, with more time and resources, could be a serious option for a propulsive landing on Earth. One of the largest abstractions from the Moon mission that the mission here on Earth will have to deal with is the lack of Throttling engines since any propulsion system outside of model rocket motors is well beyond the scope of this Capstone.\n\n## Future Work\n\nAfter determining that solid model rocket motors are the best option for the current mission scope, the next step is determining what motor to use. There are many great options, and deciding what thrust profile is ideal may have to wait until a Simulink simulation of the landing can be built so that the metrics of each motor can be constrained more. Instead of throttling motors, the current working idea is that thrust vector control may be a way to squeeze a little more control out of a solid rocket motor. Thrust Vector Control will undoubtedly be challenging to control, so another essential piece that needs exploring is whether an LQR controller is feasible or if a PID controller is accurate enough to control our system. \n\n\nCodebase: [https://gitlab.com/lander-team/air-prop-simulation](https://gitlab.com/lander-team/air-prop-simulation)\n\n",
+ "markdown": "---\ntitle: \"Air Propulsion Simulation\"\ndescription: |\n Simulating the performance of a compressed air propulsion system as an alternative to solid rocket motors using Julia.\ndescription-meta: |\n Simulate air propulsion for lunar mining transport! This project explores using compressed air as an alternative to solid rocket motors. See the Julia simulation results and comparisons with traditional rocket motor performance. Explore the code and learn more about this innovative approach.\nrepository_url: https://gitlab.com/lander-team/air-prop-simulation\ndate: 2021-04-01\ndate-modified: 2024-03-10\ncategories:\n - Julia\n - Capstone\n - University\n - Code\n - Aerospace\n - Math\ncreative_commons: CC BY\nbanner: prop_comp.png\nimage-alt: A line graph comparing the thrust of different propulsion systems over time. The x-axis represents time in seconds, and the y-axis represents thrust in Newtons. The graph displays the thrust curves for Air Propulsion, F10, F15, and G8ST.\nformat:\n html:\n code-tools: true\n code-fold: false\nexecute:\n output: false\nfreeze: true\n---\n\nFor Capstone my team was tasked with designing a system capable of moving mining equipment and materials around the surface of the Moon using a propulsive landing. The system had to be tested on Earth with something feasible for our team to build in 2 semesters. One of the first considerations my capstone advisor wanted was to test the feasibility of an air propulsion system instead of the obvious solution that of using solid rocket motors. This document is just _napkin math_ to determine if the system is even feasibly and is not meant to be a rigorous study of an air propulsion system that would easily keep a capstone team busy by itself. \n\n::: {#4b03dcc3 .cell execution_count=1}\n``` {.julia .cell-code code-fold=\"true\" code-summary=\"Imports\"}\nusing Plots\nplotlyjs()\n\nusing Unitful\nusing DataFrames\nusing Measurements\nusing Measurements: value, uncertainty\nusing CSV\n```\n:::\n\n\n## The Simulation\n\nI chose an off-the-shelf paintball gun tank for the pressure vessel. The primary consideration was the incredible pressure to weight ratio, and the fact that it is designed to be bumped around would be necessary for proving the safety of the system further into the project. \n\n::: {#3b0d3b37 .cell execution_count=2}\n``` {.julia .cell-code code-fold=\"false\"}\n# Tank https://www.amazon.com/Empire-Paintball-BASICS-Pressure-Compressed/dp/B07B6M48SR/\nV = (85 ± 5)u\"inch^3\";\nP0 = (4200.0 ± 300)u\"psi\";\nWtank = (2.3 ± 0.2)u\"lb\";\nPmax = (250 ± 50)u\"psi\"; # Max Pressure that can come out the nozzle\n```\n:::\n\n\nThe nozzle diameter was changed until the air prop system had a _burn time_ similar to a G18ST rocket motor. The propulsion system's total impulse is not dependant on the nozzle diameter, so this was just done to make it plot nicely with the rest of the rocket motors since, at this time, it is unknown what the optimal thrust profile is. \n\n::: {#fb49f3ca .cell execution_count=3}\n``` {.julia .cell-code}\n# Params\nd_nozzle = ((1 // 18) ± 0.001)u\"inch\";\na_nozzle = (pi / 4) * d_nozzle^2;\n```\n:::\n\n\nThese are just universal values for what a typical day would look like during the summer in Northern Arizona. [@cengel_thermodynamics]\n\n::: {#690060f1 .cell execution_count=4}\n``` {.julia .cell-code}\n# Universal Stuff\nP_amb = (1 ± 0.2)u\"atm\";\nγ = 1.4 ± 0.05;\nR = 287.05u\"J/(kg * K)\";\nT = (300 ± 20)u\"K\";\n```\n:::\n\n\nThe actual simulation is quite simple. The basic idea is that using the current pressure, you can calculate $\\dot{m}$, which allows calculating the Thrust, and then you can subtract the current mass of air in the tank by $\\dot{m}$ and recalculate pressure using the new mass then repeat the whole process.\n\n\nThe bulk of the equations in the simulation came from [@cengel_thermodynamics], while the Thrust and $v_e$ equations came from [@sutton_rocket_2001, eq: 2-14].\n\n$$ T = \\dot{m} \\cdot v_\\text{Exit} + A_\\text{Nozzle} \\cdot (P - P_\\text{Ambient}) $$\n\nThe initial pressure difference is 4190.0 ± 300.0 psi, which is massive, so the area of the nozzle significantly alters the thrust profile. The paintball tanks come with pressure regulators, in our case, 800 psi which is still a huge number compared to atmospheric pressure. While the total impulse of the system doesn't change with different nozzle areas, the peak thrust and _burn time_ vary greatly. One of the benefits of doing air propulsion and the reason it was even considered so seriously is that it should be possible to change the nozzle diameter in flight, allowing thrust to be throttled, making controlled landing easier to control. \n\n::: {#f812ad33 .cell execution_count=5}\n``` {.julia .cell-code}\nt = 0.0u\"s\";\nP = P0 |> u\"Pa\";\nM = V * (P / (R * T)) |> u\"kg\";\nts = 1u\"ms\";\ndf = DataFrame(Thrust=(0 ± 0)u\"N\", Pressure=P0, Time=0.0u\"s\", Mass=M);\nwhile M > 0.005u\"kg\"\n # Calculate what is leaving tank\n P = minimum([P, Pmax])\n ve = sqrt((2 * γ / (γ - 1)) * R * T * (1 - P_amb / P)^((γ - 1) / γ)) |> u\"m/s\"\n ρ = P / (R * T) |> u\"kg/m^3\"\n ṁ = ρ * a_nozzle * ve |> u\"kg/s\"\n\n Thrust = ṁ * ve + a_nozzle * (P - P_amb) |> u\"N\"\n\n # Calculate what is still in the tank\n M = M - ṁ * ts |> u\"kg\"\n P = (M * R * T) / V |> u\"Pa\"\n t = t + ts\n\n df_step = DataFrame(Thrust=Thrust, Pressure=P, Time=t, Mass=M)\n append!(df, df_step)\nend\n```\n:::\n\n\n## Analysis\n\n\nBelow in figure 1, the result of the simulation is plotted. Notice the massive error once the tank starts running low. This is because the calculation for pressure has a lot of very uncertain variables. This is primarily due to air being a compressible fluid, making this simulation challenging to do accurately. The thrust being below 0 N might not make intuitive sense, but it's technically possible for the pressure to compress, leaving the inside of the rocket nozzle with a pressure that's actually below atmospheric pressure. The effect would likely last a fraction of a second, but the point stands that this simulation is wildly inaccurate and only meant to get an idea of what an air propulsion system is capable of. \n\n::: {#3ed8adc4 .cell class-output='preview-image' code-folding='true' execution_count=6}\n``` {.julia .cell-code}\nthrust_values = df.Thrust .|> ustrip .|> value;\nthrust_uncertainties = df.Thrust .|> ustrip .|> uncertainty;\ntime_values = df.Time .|> ustrip;\n\n# Downsample for plotting (keep ~1000 points for visual fidelity)\nn_points = length(thrust_values)\nstep = max(1, n_points ÷ 1000)\nidx = 1:step:n_points\n\nair = DataFrame(\n Thrust=thrust_values[idx],\n Uncertainty=thrust_uncertainties[idx],\n Time=time_values[idx]\n);\n\nplot(air.Time, air.Thrust,\n title=\"Thrust Over Time\",\n ribbon=(air.Uncertainty, air.Uncertainty),\n fillalpha=0.2, label=\"Thrust\",\n xlabel=\"Time (s)\",\n ylabel=\"Thrust (N)\",\n)\n```\n\n::: {.cell-output .cell-output-display execution_count=8}\n```{=html}\n \n \n```\n\nAir Propulsion Simulation\n:::\n:::\n\n\nIn Figure 2, the air propulsion simulation is compared to commercially available rocket motors. This early in the project, we have no idea whether short burns or longer burns are ideal for a propulsive landing, so the air propulsion system was compared to a variety of different motors with unique profiles. \n\n::: {#cc00dad6 .cell code-folding='tru' execution_count=7}\n``` {.julia .cell-code}\nf10 = CSV.read(\"AeroTech_F10.csv\", DataFrame);\nf15 = CSV.read(\"Estes_F15.csv\", DataFrame);\ng8 = CSV.read(\"AeroTech_G8ST.csv\", DataFrame);\n\nplot(air.Time, air.Thrust, label=\"Air Propulsion\", legend=:outertopright);\n\nfor (d, l) in [(f10, \"F10\"), (f15, \"F15\"), (g8, \"G8ST\")]\n plot!(d[!, \"Time (s)\"], d[!, \"Thrust (N)\"], label=l)\nend\n\ntitle!(\"Propulsion Comparison\");\nxlabel!(\"Time (s)\");\nylabel!(\"Thrust (N)\")\n```\n\n::: {.cell-output .cell-output-display execution_count=9}\n```{=html}\n \n \n```\n\nRocket Motor Data: [@thrustcurve]\n:::\n:::\n\n\nIn the end, the air propulsion system's performance has a very impressive total impulse and, with more time and resources, could be a serious option for a propulsive landing on Earth. One of the largest abstractions from the Moon mission that the mission here on Earth will have to deal with is the lack of Throttling engines since any propulsion system outside of model rocket motors is well beyond the scope of this Capstone.\n\n## Future Work\n\nAfter determining that solid model rocket motors are the best option for the current mission scope, the next step is determining what motor to use. There are many great options, and deciding what thrust profile is ideal may have to wait until a Simulink simulation of the landing can be built so that the metrics of each motor can be constrained more. Instead of throttling motors, the current working idea is that thrust vector control may be a way to squeeze a little more control out of a solid rocket motor. Thrust Vector Control will undoubtedly be challenging to control, so another essential piece that needs exploring is whether an LQR controller is feasible or if a PID controller is accurate enough to control our system. \n\n\nCodebase: [https://gitlab.com/lander-team/air-prop-simulation](https://gitlab.com/lander-team/air-prop-simulation)\n\n",
"supporting": [
"index_files"
],
diff --git a/Anson-Projects/projects/_freeze/posts/2021-04-14-iss-eclipse-determination/index/execute-results/html.json b/Anson-Projects/projects/_freeze/posts/2021-04-14-iss-eclipse-determination/index/execute-results/html.json
index a6b33a102..8526dbe8d 100644
--- a/Anson-Projects/projects/_freeze/posts/2021-04-14-iss-eclipse-determination/index/execute-results/html.json
+++ b/Anson-Projects/projects/_freeze/posts/2021-04-14-iss-eclipse-determination/index/execute-results/html.json
@@ -1,8 +1,8 @@
{
- "hash": "89ff49a6b9e2d75bb7f69c6ed2418a3a",
+ "hash": "aa1423020fa8b85658b268c0cbdd9cf2",
"result": {
"engine": "jupyter",
- "markdown": "---\ntitle: \"ISS Eclipse Determination\"\ndescription: |\n Calculate sunlight exposure for orbiting spacecraft like the ISS. This Julia project demonstrates how to determine eclipse times, considering umbra, penumbra, and full sunlight. Visualizations and code included. Learn about orbital mechanics and mission design considerations.\ndate: 2021-05-01\ndate-modified: 2024-02-292\ncategories:\n - Julia\n - Astrodynamics\n - Code\n - Aerospace\n - Notes\n - Space\n - Math\ncreative_commons: CC BY\nimage: preview.png\nimage-alt: A diagram illustrating the umbra and penumbra regions cast by a celestial body.\nformat:\n html:\n code-tools: true\n code-fold: false\nfreeze: true\n---\n\n\n\n\nDetermining the eclipses a satellite will encounter is a major driving factor when designing a mission in space. Thermal and power budgets have to be made with the fact that a satellite will periodically be in the complete darkness of space where it will receive no solar radiation to power the solar panels and keep the spacecraft from freezing.\n\n## What is an Eclipse\n\n{fig-alt=\"A diagram of an eclipse. The sun is shown as a large yellow circle on the left. A smaller blue circle labeled \"Body\" is to the right of the sun. A spacecraft is shown above the body. The umbra and penumbra are labeled.\"}\n\nThe above image is a simple representation of what an eclipse is. First, you'll notice the Umbra is complete darkness, then the Penumbra, which is a shadow of varying darkness, and then the rest of the orbit is in full sunlight. For this example, I will be using the ISS, which has a very low orbit, so the Penumbra isn't much of a problem. However, you can tell by looking at the diagram that higher altitude orbits would spend more time in the Penumbra. \n\n{fig-alt=\"A diagram expanding on the last figure, but with distances marked for the radii of the sun and body, and the distance between the body and spacecraft.\"}\n\nHere is a more detailed view of the eclipse that will make it easier to explain what is going on. There are 2 Position vectors, and 2 radius that need to be known for simple eclipse determination. More advanced cases where the atmosphere of the body your orbiting can significantly affect the Umbra and Penumbra, and other bodies could also potentially block the Sun. However, we will keep it simple for this example since they have minimal effect on the ISS’s orbit. Rsun and Rbody are the radius of the Sun and Body (In this case Earth), respectively. r_sun_body is a vector from the center of the Sun to the center of the target body. For this example I will only be using one vector, but for more rigorous eclipse determination it is important to calculate the ephemeris at least once a day since it does significantly change over the course of a year. The reason that I am ignoring it at the moment is because there is currently no good way to calculate [Ephemerides](https://ssd.jpl.nasa.gov/?ephemerides) in Julia but the package is being worked on so I may revisit this and do a more rigorous analysis in the future. r_body_sc is a position vector from the center of the body being orbited, to the center of our spacecraft. \n\n## The Code\n\n::: {#58f0a5bd .cell execution_count=1}\n``` {.julia .cell-code code-fold=\"true\" code-summary=\"Imports\"}\nusing Unitful\nusing LinearAlgebra\nusing SatelliteToolbox\nusing Plots\nusing Colors\ntheme(:ggplot2)\n```\n:::\n\n\nTo get the orbit for the ISS, I used a [Two-Line Element](https://en.wikipedia.org/wiki/Two-line_element_set) which is a data format for explaining orbits. The US Joint Space Operations Center makes these widely available, but https://live.ariss.org/tle/ makes the TLE for the ISS way more accessible [@ariss]. The Julia Package [SatelliteToolbox.jl](https://github.com/JuliaSpace/SatelliteToolbox.jl) makes it super easy to turn a TLE into an orbit that can be propagated. Simply putting the TLE in a string and using the `tle` string macro like below, we now have access to the information to start making our ISS orbit. \n\n::: {#05dfbdce .cell execution_count=2}\n``` {.julia .cell-code}\nISS = tle\"\"\"\nISS (ZARYA)\n1 25544U 98067A 21103.84943184 .00000176 00000-0 11381-4 0 9990\n2 25544 51.6434 300.9481 0002858 223.8443 263.8789 15.48881793278621\n\"\"\"\n```\n\n::: {.cell-output .cell-output-display execution_count=3}\n\n::: {.ansi-escaped-output}\n```{=html}\n
TLE:\n Name : ISS (ZARYA)\n Satellite number : 25544\n International designator : 98067A\n Epoch (Year / Day) : 21 / 103.84943184 (2021-04-13T20:23:10.911)\n Element set number : 999\n Eccentricity : 0.00028580\n Inclination : 51.64340000 deg\n RAAN : 300.94810000 deg\n Argument of perigee : 223.84430000 deg\n Mean anomaly : 263.87890000 deg\n Mean motion (n) : 15.48881793 revs / day\n Revolution number : 27862\n B* : 1.1381e-05 1 / er\n ṅ / 2 : 1.76e-06 rev / day²\n n̈ / 6 : 0 rev / day³
\n```\n:::\n\n:::\n:::\n\n\nNow that we have the TLE, we can pass that into SatelliteToolbox's orbit propagator. Before propagating the orbit, we need to have a range of time steps to pass into the propagator. The TLE gives the mean motion, n, which is the revolutions per day, so using that, we can calculate the amount of time required for one orbit, which is all that we're worried about for this analysis. The propagator returns a tuple containing the Orbital elements, a position vector with units meters, and a velocity vector with units meters per second. For this analysis were only worried about the position vector. \n\n::: {#ac9f27f7 .cell execution_count=3}\n``` {.julia .cell-code}\nISS.mean_motion\n```\n\n::: {.cell-output .cell-output-display execution_count=4}\n```\n15.48881793\n```\n:::\n:::\n\n\n::: {#8a13274c .cell execution_count=4}\n``` {.julia .cell-code}\norbit = Propagators.init(Val(:SGP4), ISS);\ntime = 0:0.1:((24/ISS.mean_motion).*60*60);\npropagated = Propagators.propagate!.(orbit, time);\nr = first.(propagated); # Get distance from propagator\n```\n:::\n\n\nWe just need to use the radii and vectors discussed earlier to determine if the ISS is in the penumbra or umbra. This is a lot of trigonometry and vector math that I won't bore anyone with. However, using the diagrams above and following the code in the sunlight function, you should follow what's happening. For a rigorous discussion, check out [@vallado]. \n\n::: {#88b06063 .cell execution_count=5}\n``` {.julia .cell-code}\nfunction sunlight(Rbody, r_sun_body, r_body_sc)\n Rsun = 695_700u\"km\"\n\n hu = Rbody * norm(r_sun_body) / (Rsun - Rbody)\n\n θe = acos((r_sun_body ⋅ r_body_sc) / (norm(r_sun_body) * norm(r_body_sc)))\n\n θu = atan(Rbody / hu)\n du = hu * sin(θu) / sin(θe + θu)\n\n θp = π - atan(norm(r_sun_body) / (Rsun + Rbody))\n dp = Rbody * sin(θp) / cos(θe - θp)\n\n S = 1\n if (θe < π / 2) && (norm(r_body_sc) < du)\n S = 0\n end\n if (θe < π / 2) && ((du < norm(r_body_sc)) && (norm(r_body_sc) < dp))\n S = (norm(r_body_sc .|> u\"km\") - du) / (dp - du) |> ustrip\n end\n\n return S\nend\n```\n\n::: {.cell-output .cell-output-display execution_count=6}\n```\nsunlight (generic function with 1 method)\n```\n:::\n:::\n\n\nThen we can pass all the values we've gathered into the function we just made. \n\n::: {#bed4435d .cell execution_count=6}\n``` {.julia .cell-code}\nS = r .|> R -> sunlight(6371u\"km\", [0.5370, 1.2606, 0.5466] .* 1e8u\"km\", R .* u\"m\");\n```\n:::\n\n\n## Plotting the Results\n\nThe `sunlight` function returns values from 0 to 1, 0 being complete darkness, 1 being complete sunlight, and anything between being the fraction of light being received. Again since the ISS has a very low orbit, the amount of time spent in the penumbra is almost insignificant. \n\n::: {#174a4492 .cell alt-text='A graph titled \"ISS Sunlight Over a Day\" showing the percentage of sunlight the ISS receives over a 24-hour period. The x-axis represents time in hours, and the y-axis represents sunlight percentage. The graph shows a period of near-total sunlight followed by a period of darkness, and the cycle repeats.' execution_count=7}\n``` {.julia .cell-code code-fold=\"true\"}\n# Get fancy with the line color. \nlight_range = range(colorant\"black\", stop=colorant\"orange\", length=101);\nlight_colors = [light_range[unique(round(Int, 1 + s * 100))][1] for s in S];\n\nplot(\n LinRange(0, 24, length(S)),\n S .* 100,\n linewidth=5,\n legend=false,\n color=light_colors,\n);\n\nxlabel!(\"Time (hr)\");\nylabel!(\"Sunlight (%)\");\ntitle!(\"ISS Sunlight Over a Day\")\n```\n\n::: {.cell-output .cell-output-display execution_count=8}\n```{=html}\n\n\n```\n\nISS Sunlight\n:::\n:::\n\n\nLooking at the plot, the vertical transition from 0% to 100% makes it pretty clear that the time in the penumbra is limited. Still, almost counterintuitively, it also looks like the ISS gets more sunlight than it does darkness. So, using the raw sunlight data, we can calculate precisely how much time is spent in each region. \n\nTime in Sun:\n\n::: {#0bf6b884 .cell execution_count=8}\n``` {.julia .cell-code}\nsun = length(S[S.==1]) / length(S) * 100\n```\n\n::: {.cell-output .cell-output-display execution_count=9}\n```\n62.04757721886597\n```\n:::\n:::\n\n\nTime in Darkness:\n\n::: {#6cca15d4 .cell execution_count=9}\n``` {.julia .cell-code}\numbra = length(S[S.==0]) / length(S) * 100\n```\n\n::: {.cell-output .cell-output-display execution_count=10}\n```\n37.627951167918546\n```\n:::\n:::\n\n\nTime in Penumbra:\n\n::: {#29ef8e5a .cell execution_count=10}\n``` {.julia .cell-code}\npenumbra = 100 - umbra - sun\n```\n\n::: {.cell-output .cell-output-display execution_count=11}\n```\n0.32447161321548634\n```\n:::\n:::\n\n\nThis means that even with the ISS's low orbit, it still gets sunlight ~62% of the time and spends almost no time in the penumbra. This would vary a few percent depending on the time of year, but in a circular orbit like the ISS, the amount of sunlight would remain pretty constant. There are other orbits like a polar orbit, lunar orbit, or highly elliptic earth orbits that can have their time in the sunlight vary widely by the time of year. \n\n",
+ "markdown": "---\ntitle: \"ISS Eclipse Determination\"\ndescription: |\n Calculate sunlight exposure for orbiting spacecraft like the ISS. This Julia project demonstrates how to determine eclipse times, considering umbra, penumbra, and full sunlight. Visualizations and code included. Learn about orbital mechanics and mission design considerations.\ndate: 2021-05-01\ndate-modified: 2024-02-292\ncategories:\n - Julia\n - Astrodynamics\n - Code\n - Aerospace\n - Notes\n - Space\n - Math\ncreative_commons: CC BY\nimage: preview.png\nimage-alt: A diagram illustrating the umbra and penumbra regions cast by a celestial body.\nformat:\n html:\n code-tools: true\n code-fold: false\nfreeze: true\n---\n\nDetermining the eclipses a satellite will encounter is a major driving factor when designing a mission in space. Thermal and power budgets have to be made with the fact that a satellite will periodically be in the complete darkness of space where it will receive no solar radiation to power the solar panels and keep the spacecraft from freezing.\n\n## What is an Eclipse\n\n{fig-alt=\"A diagram of an eclipse. The sun is shown as a large yellow circle on the left. A smaller blue circle labeled \"Body\" is to the right of the sun. A spacecraft is shown above the body. The umbra and penumbra are labeled.\"}\n\nThe above image is a simple representation of what an eclipse is. First, you'll notice the Umbra is complete darkness, then the Penumbra, which is a shadow of varying darkness, and then the rest of the orbit is in full sunlight. For this example, I will be using the ISS, which has a very low orbit, so the Penumbra isn't much of a problem. However, you can tell by looking at the diagram that higher altitude orbits would spend more time in the Penumbra. \n\n{fig-alt=\"A diagram expanding on the last figure, but with distances marked for the radii of the sun and body, and the distance between the body and spacecraft.\"}\n\nHere is a more detailed view of the eclipse that will make it easier to explain what is going on. There are 2 Position vectors, and 2 radius that need to be known for simple eclipse determination. More advanced cases where the atmosphere of the body your orbiting can significantly affect the Umbra and Penumbra, and other bodies could also potentially block the Sun. However, we will keep it simple for this example since they have minimal effect on the ISS’s orbit. Rsun and Rbody are the radius of the Sun and Body (In this case Earth), respectively. r_sun_body is a vector from the center of the Sun to the center of the target body. For this example I will only be using one vector, but for more rigorous eclipse determination it is important to calculate the ephemeris at least once a day since it does significantly change over the course of a year. The reason that I am ignoring it at the moment is because there is currently no good way to calculate [Ephemerides](https://ssd.jpl.nasa.gov/?ephemerides) in Julia but the package is being worked on so I may revisit this and do a more rigorous analysis in the future. r_body_sc is a position vector from the center of the body being orbited, to the center of our spacecraft. \n\n## The Code\n\n::: {#f92c4c22 .cell execution_count=1}\n``` {.julia .cell-code code-fold=\"true\" code-summary=\"Imports\"}\nusing Unitful\nusing LinearAlgebra\nusing SatelliteToolbox\nusing Plots\nusing Colors\ntheme(:ggplot2)\n```\n:::\n\n\nTo get the orbit for the ISS, I used a [Two-Line Element](https://en.wikipedia.org/wiki/Two-line_element_set) which is a data format for explaining orbits. The US Joint Space Operations Center makes these widely available, but https://live.ariss.org/tle/ makes the TLE for the ISS way more accessible [@ariss]. The Julia Package [SatelliteToolbox.jl](https://github.com/JuliaSpace/SatelliteToolbox.jl) makes it super easy to turn a TLE into an orbit that can be propagated. Simply putting the TLE in a string and using the `tle` string macro like below, we now have access to the information to start making our ISS orbit. \n\n::: {#d8a9cdaf .cell execution_count=2}\n``` {.julia .cell-code}\nISS = tle\"\"\"\nISS (ZARYA)\n1 25544U 98067A 21103.84943184 .00000176 00000-0 11381-4 0 9990\n2 25544 51.6434 300.9481 0002858 223.8443 263.8789 15.48881793278621\n\"\"\"\n```\n\n::: {.cell-output .cell-output-display execution_count=3}\n\n::: {.ansi-escaped-output}\n```{=html}\n
TLE:\n Name : ISS (ZARYA)\n Satellite number : 25544\n International designator : 98067A\n Epoch (Year / Day) : 21 / 103.84943184 (2021-04-13T20:23:10.911)\n Element set number : 999\n Eccentricity : 0.00028580\n Inclination : 51.64340000 deg\n RAAN : 300.94810000 deg\n Argument of perigee : 223.84430000 deg\n Mean anomaly : 263.87890000 deg\n Mean motion (n) : 15.48881793 revs / day\n Revolution number : 27862\n B* : 1.1381e-05 1 / er\n ṅ / 2 : 1.76e-06 rev / day²\n n̈ / 6 : 0 rev / day³
\n```\n:::\n\n:::\n:::\n\n\nNow that we have the TLE, we can pass that into SatelliteToolbox's orbit propagator. Before propagating the orbit, we need to have a range of time steps to pass into the propagator. The TLE gives the mean motion, n, which is the revolutions per day, so using that, we can calculate the amount of time required for one orbit, which is all that we're worried about for this analysis. The propagator returns a tuple containing the Orbital elements, a position vector with units meters, and a velocity vector with units meters per second. For this analysis were only worried about the position vector. \n\n::: {#b25e8aec .cell execution_count=3}\n``` {.julia .cell-code}\nISS.mean_motion\n```\n\n::: {.cell-output .cell-output-display execution_count=4}\n```\n15.48881793\n```\n:::\n:::\n\n\n::: {#a3946215 .cell execution_count=4}\n``` {.julia .cell-code}\norbit = Propagators.init(Val(:SGP4), ISS);\ntime = 0:0.1:((24/ISS.mean_motion).*60*60);\npropagated = Propagators.propagate!.(orbit, time);\nr = first.(propagated); # Get distance from propagator\n```\n:::\n\n\nWe just need to use the radii and vectors discussed earlier to determine if the ISS is in the penumbra or umbra. This is a lot of trigonometry and vector math that I won't bore anyone with. However, using the diagrams above and following the code in the sunlight function, you should follow what's happening. For a rigorous discussion, check out [@vallado]. \n\n::: {#6646946f .cell execution_count=5}\n``` {.julia .cell-code}\nfunction sunlight(Rbody, r_sun_body, r_body_sc)\n Rsun = 695_700u\"km\"\n\n hu = Rbody * norm(r_sun_body) / (Rsun - Rbody)\n\n θe = acos((r_sun_body ⋅ r_body_sc) / (norm(r_sun_body) * norm(r_body_sc)))\n\n θu = atan(Rbody / hu)\n du = hu * sin(θu) / sin(θe + θu)\n\n θp = π - atan(norm(r_sun_body) / (Rsun + Rbody))\n dp = Rbody * sin(θp) / cos(θe - θp)\n\n S = 1\n if (θe < π / 2) && (norm(r_body_sc) < du)\n S = 0\n end\n if (θe < π / 2) && ((du < norm(r_body_sc)) && (norm(r_body_sc) < dp))\n S = (norm(r_body_sc .|> u\"km\") - du) / (dp - du) |> ustrip\n end\n\n return S\nend\n```\n\n::: {.cell-output .cell-output-display execution_count=6}\n```\nsunlight (generic function with 1 method)\n```\n:::\n:::\n\n\nThen we can pass all the values we've gathered into the function we just made. \n\n::: {#ac28930b .cell execution_count=6}\n``` {.julia .cell-code}\nS = r .|> R -> sunlight(6371u\"km\", [0.5370, 1.2606, 0.5466] .* 1e8u\"km\", R .* u\"m\");\n```\n:::\n\n\n## Plotting the Results\n\nThe `sunlight` function returns values from 0 to 1, 0 being complete darkness, 1 being complete sunlight, and anything between being the fraction of light being received. Again since the ISS has a very low orbit, the amount of time spent in the penumbra is almost insignificant. \n\n::: {#7069bc07 .cell alt-text='A graph titled \"ISS Sunlight Over a Day\" showing the percentage of sunlight the ISS receives over a 24-hour period. The x-axis represents time in hours, and the y-axis represents sunlight percentage. The graph shows a period of near-total sunlight followed by a period of darkness, and the cycle repeats.' execution_count=7}\n``` {.julia .cell-code code-fold=\"true\"}\n# Downsample for plotting (keep ~1000 points for visual fidelity)\nn_points = length(S)\nstep = max(1, n_points ÷ 1000)\nidx = 1:step:n_points\nS_plot = S[idx]\n\n# Get fancy with the line color.\nlight_range = range(colorant\"black\", stop=colorant\"orange\", length=101);\nlight_colors = [light_range[unique(round(Int, 1 + s * 100))][1] for s in S_plot];\n\nplot(\n LinRange(0, 24, length(S_plot)),\n S_plot .* 100,\n linewidth=5,\n legend=false,\n color=light_colors,\n);\n\nxlabel!(\"Time (hr)\");\nylabel!(\"Sunlight (%)\");\ntitle!(\"ISS Sunlight Over a Day\")\n```\n\n::: {.cell-output .cell-output-display execution_count=8}\n```{=html}\n\n```\n\nISS Sunlight\n:::\n:::\n\n\nLooking at the plot, the vertical transition from 0% to 100% makes it pretty clear that the time in the penumbra is limited. Still, almost counterintuitively, it also looks like the ISS gets more sunlight than it does darkness. So, using the raw sunlight data, we can calculate precisely how much time is spent in each region. \n\nTime in Sun:\n\n::: {#f7fe5f20 .cell execution_count=8}\n``` {.julia .cell-code}\nsun = length(S[S.==1]) / length(S) * 100\n```\n\n::: {.cell-output .cell-output-display execution_count=9}\n```\n62.04757721886597\n```\n:::\n:::\n\n\nTime in Darkness:\n\n::: {#051e5dc2 .cell execution_count=9}\n``` {.julia .cell-code}\numbra = length(S[S.==0]) / length(S) * 100\n```\n\n::: {.cell-output .cell-output-display execution_count=10}\n```\n37.627951167918546\n```\n:::\n:::\n\n\nTime in Penumbra:\n\n::: {#125c4904 .cell execution_count=10}\n``` {.julia .cell-code}\npenumbra = 100 - umbra - sun\n```\n\n::: {.cell-output .cell-output-display execution_count=11}\n```\n0.32447161321548634\n```\n:::\n:::\n\n\nThis means that even with the ISS's low orbit, it still gets sunlight ~62% of the time and spends almost no time in the penumbra. This would vary a few percent depending on the time of year, but in a circular orbit like the ISS, the amount of sunlight would remain pretty constant. There are other orbits like a polar orbit, lunar orbit, or highly elliptic earth orbits that can have their time in the sunlight vary widely by the time of year. \n\n",
"supporting": [
"index_files"
],
diff --git a/Anson-Projects/projects/_quarto.yml b/Anson-Projects/projects/_quarto.yml
index c50a2c2bd..100d0f588 100644
--- a/Anson-Projects/projects/_quarto.yml
+++ b/Anson-Projects/projects/_quarto.yml
@@ -5,49 +5,36 @@ website:
title: "Anson's Projects"
site-url: https://projects.ansonbiggs.com
description: A Blog for Technical Topics
+ announcement:
+ icon: info-circle
+ dismissable: false
+ content: "For the best experience, check out these posts on my main blog: [notes.ansonbiggs.com](https://notes.ansonbiggs.com)"
+ type: primary
+ position: below-navbar
+ navbar:
+ left:
+ - text: "Blog"
+ href: https://notes.ansonbiggs.com
+ - text: "About"
+ href: about.html
+ right:
+ - icon: rss
+ href: index.xml
+ # - icon: gitlab
+ # href: https://gitlab.com/MisterBiggs
+ open-graph: true
-profiles:
- default:
- website:
- title: "Anson's Projects"
- site-url: https://projects.ansonbiggs.com
- description: A Blog for Technical Topics
- author: "Anson Biggs"
- navbar:
- left:
- - text: "About"
- href: about.html
- right:
- - icon: rss
- href: index.xml
- # - icon: gitlab
- # href: https://gitlab.com/MisterBiggs
- open-graph: true
- feed:
- title: "Anson's Projects"
- description: "A Blog for Technical Topics"
- author: "Anson Biggs"
- items: 10
- format:
- html:
- theme: zephyr
- css: styles.css
- # toc: true
-
- ghost:
- website:
- title: "Anson's Projects"
- site-url: https://projects.ansonbiggs.com
- description: A Blog for Technical Topics
- navbar: false
- open-graph: true
- format:
- html:
- theme: none
- css: ghost-iframe.css
- toc: false
- page-layout: article
- title-block-banner: false
+format:
+ html:
+ theme: zephyr
+ css: styles.css
+ author: "Anson Biggs"
+ # toc: true
-execute:
- freeze: true
\ No newline at end of file
+feed:
+ title: "Anson's Projects"
+ description: "A Blog for Technical Topics"
+ items: 10
+
+execute:
+ freeze: true
diff --git a/Anson-Projects/projects/ghost-upload/.env.example b/Anson-Projects/projects/ghost-upload/.env.example
new file mode 100644
index 000000000..7b90cd3c0
--- /dev/null
+++ b/Anson-Projects/projects/ghost-upload/.env.example
@@ -0,0 +1,2 @@
+# Ghost Admin API key (format: id:secret)
+GHOST_TOKEN=your_ghost_admin_api_key_here
diff --git a/Anson-Projects/projects/ghost-upload/.gitignore b/Anson-Projects/projects/ghost-upload/.gitignore
new file mode 100644
index 000000000..1f4ec9525
--- /dev/null
+++ b/Anson-Projects/projects/ghost-upload/.gitignore
@@ -0,0 +1,5 @@
+# Build artifacts
+target/
+
+# Environment variables (contains secrets)
+.env
diff --git a/Anson-Projects/projects/ghost-upload/.gitlab-ci.yml b/Anson-Projects/projects/ghost-upload/.gitlab-ci.yml
index 35654376f..0eac8b753 100644
--- a/Anson-Projects/projects/ghost-upload/.gitlab-ci.yml
+++ b/Anson-Projects/projects/ghost-upload/.gitlab-ci.yml
@@ -3,8 +3,9 @@ publish:
image: rust:latest
script:
- cd ./ghost-upload
- - cargo run
+ - cargo run --release
needs:
- - pages
+ - job: pages
+ artifacts: true
rules:
- if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"
diff --git a/Anson-Projects/projects/ghost-upload/Cargo.lock b/Anson-Projects/projects/ghost-upload/Cargo.lock
index 99a376f41..d427be103 100644
--- a/Anson-Projects/projects/ghost-upload/Cargo.lock
+++ b/Anson-Projects/projects/ghost-upload/Cargo.lock
@@ -1,34 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
-version = 3
-
-[[package]]
-name = "addr2line"
-version = "0.24.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
-dependencies = [
- "gimli",
-]
-
-[[package]]
-name = "adler2"
-version = "2.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
-
-[[package]]
-name = "ahash"
-version = "0.8.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
-dependencies = [
- "cfg-if",
- "getrandom 0.2.15",
- "once_cell",
- "version_check",
- "zerocopy",
-]
+version = 4
[[package]]
name = "aho-corasick"
@@ -55,49 +27,72 @@ dependencies = [
]
[[package]]
-name = "arrayref"
-version = "0.3.9"
+name = "anstream"
+version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
+checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
[[package]]
-name = "arrayvec"
-version = "0.5.2"
+name = "anstyle"
+version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
+checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
-name = "atomic-waker"
-version = "1.1.2"
+name = "anstyle-parse"
+version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
+dependencies = [
+ "utf8parse",
+]
[[package]]
-name = "autocfg"
-version = "1.4.0"
+name = "anstyle-query"
+version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
[[package]]
-name = "backtrace"
-version = "0.3.74"
+name = "anstyle-wincon"
+version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
- "addr2line",
- "cfg-if",
- "libc",
- "miniz_oxide",
- "object",
- "rustc-demangle",
- "windows-targets",
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
]
[[package]]
-name = "base64"
-version = "0.13.1"
+name = "anyhow"
+version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "base64"
@@ -117,17 +112,6 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
-[[package]]
-name = "blake2b_simd"
-version = "0.5.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
-dependencies = [
- "arrayref",
- "arrayvec",
- "constant_time_eq",
-]
-
[[package]]
name = "bumpalo"
version = "3.16.0"
@@ -173,23 +157,54 @@ dependencies = [
"num-traits",
"serde",
"wasm-bindgen",
- "windows-targets",
+ "windows-targets 0.52.6",
]
[[package]]
-name = "clippy"
-version = "0.0.302"
+name = "clap"
+version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d911ee15579a3f50880d8c1d59ef6e79f9533127a3bd342462f5d584f5e8c294"
+checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
dependencies = [
- "term",
+ "clap_builder",
+ "clap_derive",
]
[[package]]
-name = "constant_time_eq"
-version = "0.1.5"
+name = "clap_builder"
+version = "4.5.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
+checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "core-foundation"
@@ -207,12 +222,6 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
-[[package]]
-name = "crossbeam-utils"
-version = "0.8.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
-
[[package]]
name = "cssparser"
version = "0.34.0"
@@ -256,17 +265,6 @@ dependencies = [
"syn",
]
-[[package]]
-name = "dirs"
-version = "1.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901"
-dependencies = [
- "libc",
- "redox_users",
- "winapi",
-]
-
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -278,6 +276,12 @@ dependencies = [
"syn",
]
+[[package]]
+name = "dotenvy"
+version = "0.15.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+
[[package]]
name = "dtoa"
version = "1.0.9"
@@ -295,9 +299,9 @@ dependencies = [
[[package]]
name = "ego-tree"
-version = "0.9.0"
+version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c6ba7d4eec39eaa9ab24d44a0e73a7949a1095a8b3f3abb11eddf27dbb56a53"
+checksum = "b2972feb8dffe7bc8c5463b1dacda1b0dfbed3710e50f977d965429692d74cd8"
[[package]]
name = "encoding_rs"
@@ -332,9 +336,9 @@ checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
[[package]]
name = "feed-rs"
-version = "2.2.0"
+version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d57df0b823b3055c667ddde499736e0d8d7d486b269454eebd96a0fdd59ce051"
+checksum = "e4c0591d23efd0d595099af69a31863ac1823046b1b021e3b06ba3aae7e00991"
dependencies = [
"chrono",
"mediatype",
@@ -370,9 +374,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
-version = "1.2.1"
+version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
@@ -494,17 +498,6 @@ dependencies = [
"unicode-width",
]
-[[package]]
-name = "getrandom"
-version = "0.1.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
-dependencies = [
- "cfg-if",
- "libc",
- "wasi 0.9.0+wasi-snapshot-preview1",
-]
-
[[package]]
name = "getrandom"
version = "0.2.15"
@@ -514,7 +507,7 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
- "wasi 0.11.0+wasi-snapshot-preview1",
+ "wasi",
"wasm-bindgen",
]
@@ -522,26 +515,26 @@ dependencies = [
name = "ghost-upload"
version = "0.1.0"
dependencies = [
+ "anyhow",
+ "base64 0.22.1",
"chrono",
- "clippy",
+ "clap",
+ "dotenvy",
"feed-rs",
"futures",
"hex",
"jsonwebtoken",
"maud",
+ "regex",
"reqwest",
"scraper",
"serde",
"serde_json",
"tokio",
+ "tracing",
+ "tracing-subscriber",
]
-[[package]]
-name = "gimli"
-version = "0.31.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
-
[[package]]
name = "h2"
version = "0.4.6"
@@ -567,6 +560,12 @@ version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
[[package]]
name = "hermit-abi"
version = "0.3.9"
@@ -699,7 +698,7 @@ dependencies = [
"http-body",
"hyper",
"pin-project-lite",
- "socket2",
+ "socket2 0.5.7",
"tokio",
"tower-service",
"tracing",
@@ -848,9 +847,9 @@ dependencies = [
[[package]]
name = "idna"
-version = "1.0.3"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
"idna_adapter",
"smallvec",
@@ -883,6 +882,12 @@ version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708"
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
[[package]]
name = "itoa"
version = "1.0.11"
@@ -913,11 +918,17 @@ dependencies = [
"simple_asn1",
]
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
[[package]]
name = "libc"
-version = "0.2.162"
+version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
+checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "linux-raw-sys"
@@ -967,6 +978,15 @@ dependencies = [
"tendril",
]
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
[[package]]
name = "maud"
version = "0.26.0"
@@ -1011,12 +1031,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
-name = "miniz_oxide"
-version = "0.8.0"
+name = "mime_guess"
+version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
- "adler2",
+ "mime",
+ "unicase",
]
[[package]]
@@ -1027,7 +1048,7 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
dependencies = [
"hermit-abi",
"libc",
- "wasi 0.11.0+wasi-snapshot-preview1",
+ "wasi",
"windows-sys 0.52.0",
]
@@ -1054,6 +1075,15 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "num-bigint"
version = "0.4.6"
@@ -1089,19 +1119,16 @@ dependencies = [
]
[[package]]
-name = "object"
-version = "0.36.5"
+name = "once_cell"
+version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e"
-dependencies = [
- "memchr",
-]
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
-name = "once_cell"
-version = "1.20.2"
+name = "once_cell_polyfill"
+version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "openssl"
@@ -1165,9 +1192,9 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
- "redox_syscall 0.5.7",
+ "redox_syscall",
"smallvec",
- "windows-targets",
+ "windows-targets 0.52.6",
]
[[package]]
@@ -1182,9 +1209,9 @@ dependencies = [
[[package]]
name = "percent-encoding"
-version = "2.3.1"
+version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "phf"
@@ -1374,15 +1401,9 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
- "getrandom 0.2.15",
+ "getrandom",
]
-[[package]]
-name = "redox_syscall"
-version = "0.1.57"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
-
[[package]]
name = "redox_syscall"
version = "0.5.7"
@@ -1392,22 +1413,11 @@ dependencies = [
"bitflags",
]
-[[package]]
-name = "redox_users"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
-dependencies = [
- "getrandom 0.1.16",
- "redox_syscall 0.1.57",
- "rust-argon2",
-]
-
[[package]]
name = "regex"
-version = "1.11.1"
+version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
@@ -1417,9 +1427,9 @@ dependencies = [
[[package]]
name = "regex-automata"
-version = "0.4.8"
+version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
+checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
@@ -1455,6 +1465,7 @@ dependencies = [
"js-sys",
"log",
"mime",
+ "mime_guess",
"native-tls",
"once_cell",
"percent-encoding",
@@ -1483,31 +1494,13 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
dependencies = [
"cc",
"cfg-if",
- "getrandom 0.2.15",
+ "getrandom",
"libc",
"spin",
"untrusted",
"windows-sys 0.52.0",
]
-[[package]]
-name = "rust-argon2"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
-dependencies = [
- "base64 0.13.1",
- "blake2b_simd",
- "constant_time_eq",
- "crossbeam-utils",
-]
-
-[[package]]
-name = "rustc-demangle"
-version = "0.1.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
-
[[package]]
name = "rustix"
version = "0.38.39"
@@ -1583,11 +1576,10 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "scraper"
-version = "0.21.0"
+version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0e749d29b2064585327af5038a5a8eb73aeebad4a3472e83531a436563f7208"
+checksum = "cc3d051b884f40e309de6c149734eab57aa8cc1347992710dc80bcc1c2194c15"
dependencies = [
- "ahash",
"cssparser",
"ego-tree",
"getopts",
@@ -1692,6 +1684,15 @@ dependencies = [
"stable_deref_trait",
]
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
[[package]]
name = "shlex"
version = "1.3.0"
@@ -1756,6 +1757,16 @@ dependencies = [
"windows-sys 0.52.0",
]
+[[package]]
+name = "socket2"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
+dependencies = [
+ "libc",
+ "windows-sys 0.60.2",
+]
+
[[package]]
name = "spin"
version = "0.9.8"
@@ -1794,6 +1805,12 @@ dependencies = [
"quote",
]
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
[[package]]
name = "subtle"
version = "2.6.1"
@@ -1876,17 +1893,6 @@ dependencies = [
"utf-8",
]
-[[package]]
-name = "term"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42"
-dependencies = [
- "byteorder",
- "dirs",
- "winapi",
-]
-
[[package]]
name = "thiserror"
version = "1.0.68"
@@ -1907,6 +1913,15 @@ dependencies = [
"syn",
]
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
[[package]]
name = "time"
version = "0.3.36"
@@ -1950,27 +1965,26 @@ dependencies = [
[[package]]
name = "tokio"
-version = "1.41.1"
+version = "1.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33"
+checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
dependencies = [
- "backtrace",
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
- "socket2",
+ "socket2 0.6.1",
"tokio-macros",
- "windows-sys 0.52.0",
+ "windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
-version = "2.4.0"
+version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
+checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
@@ -2019,21 +2033,63 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
-version = "0.1.40"
+version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
dependencies = [
"pin-project-lite",
+ "tracing-attributes",
"tracing-core",
]
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "tracing-core"
-version = "0.1.32"
+version = "0.1.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [
+ "matchers",
+ "nu-ansi-term",
"once_cell",
+ "regex-automata",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
]
[[package]]
@@ -2042,6 +2098,12 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+[[package]]
+name = "unicase"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
+
[[package]]
name = "unicode-ident"
version = "1.0.13"
@@ -2062,9 +2124,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
-version = "2.5.3"
+version = "2.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada"
+checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
dependencies = [
"form_urlencoded",
"idna",
@@ -2090,15 +2152,27 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
[[package]]
name = "uuid"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [
- "getrandom 0.2.15",
+ "getrandom",
]
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
[[package]]
name = "vcpkg"
version = "0.2.15"
@@ -2120,12 +2194,6 @@ dependencies = [
"try-lock",
]
-[[package]]
-name = "wasi"
-version = "0.9.0+wasi-snapshot-preview1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
-
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@@ -2209,37 +2277,21 @@ dependencies = [
"wasm-bindgen",
]
-[[package]]
-name = "winapi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
-dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
-]
-
-[[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
-
-[[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
-
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
- "windows-targets",
+ "windows-targets 0.52.6",
]
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
[[package]]
name = "windows-registry"
version = "0.2.0"
@@ -2248,7 +2300,7 @@ checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
dependencies = [
"windows-result",
"windows-strings",
- "windows-targets",
+ "windows-targets 0.52.6",
]
[[package]]
@@ -2257,7 +2309,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
- "windows-targets",
+ "windows-targets 0.52.6",
]
[[package]]
@@ -2267,7 +2319,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result",
- "windows-targets",
+ "windows-targets 0.52.6",
]
[[package]]
@@ -2276,7 +2328,7 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
- "windows-targets",
+ "windows-targets 0.52.6",
]
[[package]]
@@ -2285,7 +2337,25 @@ version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
- "windows-targets",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
]
[[package]]
@@ -2294,14 +2364,31 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_gnullvm",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
]
[[package]]
@@ -2310,48 +2397,96 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
[[package]]
name = "write16"
version = "1.0.0"
diff --git a/Anson-Projects/projects/ghost-upload/Cargo.toml b/Anson-Projects/projects/ghost-upload/Cargo.toml
index 1ddc10dae..13cd20013 100644
--- a/Anson-Projects/projects/ghost-upload/Cargo.toml
+++ b/Anson-Projects/projects/ghost-upload/Cargo.toml
@@ -3,20 +3,30 @@ name = "ghost-upload"
version = "0.1.0"
edition = "2021"
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
[dependencies]
-reqwest = { version = "0.12", features = ["json"] }
-feed-rs = "2.2"
-serde = { version = "1.0", features = ["derive"] }
-tokio = { version = "1.41", features = ["full"] }
-jsonwebtoken = "9.3"
-serde_json = "1.0"
-hex = "0.4"
+anyhow = "1.0"
+dotenvy = "0.15"
+base64 = "0.22"
chrono = "0.4"
+clap = { version = "4.5", features = ["derive"] }
+feed-rs = "2.3"
futures = "0.3"
+hex = "0.4"
+jsonwebtoken = "9.3"
maud = "0.26"
-scraper = "0.21"
+regex = "1.12"
+reqwest = { version = "0.12", features = ["json", "multipart"] }
+scraper = "0.22"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+tokio = { version = "1.48", features = ["full"] }
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+
+[lints.rust]
+unsafe_code = "forbid"
-[dev-dependencies]
-clippy = "0.0.302"
+[lints.clippy]
+all = { level = "warn", priority = -1 }
+pedantic = { level = "warn", priority = -1 }
+unwrap_used = "warn"
diff --git a/Anson-Projects/projects/ghost-upload/README.md b/Anson-Projects/projects/ghost-upload/README.md
index 2e5c87117..9fe0167a9 100644
--- a/Anson-Projects/projects/ghost-upload/README.md
+++ b/Anson-Projects/projects/ghost-upload/README.md
@@ -4,9 +4,9 @@ This tool synchronizes posts from https://projects.ansonbiggs.com to the Ghost b
## Features
-- **Clean content extraction**: Uses Quarto ghost profile to generate clean HTML instead of iframes
+- **Smart content handling**: Detects Observable JS posts and uses iframes; extracts native HTML for static posts
+- **Inline styling**: Includes scoped CSS for syntax highlighting without affecting other Ghost posts
- **Duplicate prevention**: Checks Ghost Admin API to avoid creating duplicate posts
-- **AI summaries**: Uses Kagi Summarizer for post summaries
- **Dual content rendering**: GitLab CI builds both main site and ghost-optimized versions
## How It Works
@@ -15,11 +15,12 @@ This tool synchronizes posts from https://projects.ansonbiggs.com to the Ghost b
- Main site → `public/` (normal theme with navigation)
- Ghost content → `public/ghost-content/` (minimal theme for content extraction)
-2. **Content Extraction**: Rust tool fetches clean HTML from the ghost-content version instead of using iframes
+2. **Content Extraction**:
+ - **OJS posts**: Uses iframe to embed the full Quarto page (requires OJS runtime)
+ - **Static posts**: Extracts HTML content with inline CSS for syntax highlighting
3. **Duplicate Detection**: Uses Ghost Admin API to check for existing posts by slug
## Environment Variables
-- `admin_api_key`: Ghost Admin API key (required)
-- `kagi_api_key`: Kagi Summarizer API key (required)
\ No newline at end of file
+- `GHOST_TOKEN`: Ghost Admin API key (required)
\ No newline at end of file
diff --git a/Anson-Projects/projects/ghost-upload/src/main.rs b/Anson-Projects/projects/ghost-upload/src/main.rs
index fe4601a69..ccdaec7ca 100644
--- a/Anson-Projects/projects/ghost-upload/src/main.rs
+++ b/Anson-Projects/projects/ghost-upload/src/main.rs
@@ -1,17 +1,626 @@
+use anyhow::{Context, Result};
+use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
+use clap::Parser;
use feed_rs::model::Entry;
use feed_rs::parser;
use futures::future::join_all;
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use maud::html;
-use reqwest::Client;
+use regex::Regex;
+use reqwest::{multipart, Client};
use scraper::{Html, Selector};
use serde::{Deserialize, Serialize};
-use std::env;
+use std::path::{Path, PathBuf};
+use std::sync::LazyLock;
+use tracing::{debug, error, info, warn};
+
+// =============================================================================
+// Constants
+// =============================================================================
+
+const GHOST_API_VERSION: &str = "v3";
+const GHOST_API_BASE: &str = "https://notes.ansonbiggs.com/ghost/api";
+/// Used only to convert RSS entry URLs to local file paths (no network requests)
+const SOURCE_SITE_PREFIX: &str = "https://projects.ansonbiggs.com";
+
+const TAG_PROJECTS_WEBSITE: &str = "Projects Website";
+const MAX_FEATURE_IMAGE_ALT_LEN: usize = 190;
+const MAX_EXCERPT_LEN: usize = 300;
+const JWT_EXPIRY_SECONDS: i64 = 300;
+
+// Maximum SVG size (in bytes) to convert to data URI
+// Larger SVGs will be replaced with a link to the original
+const MAX_SVG_SIZE_FOR_DATA_URI: usize = 500_000; // 500KB
+
+// =============================================================================
+// CLI Arguments
+// =============================================================================
+
+#[derive(Parser, Debug)]
+#[command(name = "ghost-upload")]
+#[command(about = "Sync posts from Quarto site to Ghost blog")]
+#[command(version)]
+struct Args {
+ /// Path to the RSS feed file
+ #[arg(short, long, default_value = "../public/index.xml")]
+ feed: String,
+
+ /// Path to content directory with rendered HTML
+ #[arg(short, long, default_value = "../public")]
+ content: String,
+
+ /// Dry run - don't actually create or update posts
+ #[arg(short = 'n', long)]
+ dry_run: bool,
+
+ /// Verbose output
+ #[arg(short, long)]
+ verbose: bool,
+
+ /// Find and delete duplicate posts (keeps the one with correct canonical URL)
+ #[arg(long)]
+ dedupe: bool,
+
+ /// Force update all posts, ignoring timestamp checks
+ #[arg(long)]
+ force: bool,
+}
+
+// =============================================================================
+// Compiled Regexes (compiled once at startup)
+// =============================================================================
+
+static RE_IMG_SRC: LazyLock = LazyLock::new(|| {
+ Regex::new(r#"src="([^"]+)""#).expect("Invalid IMG_SRC regex")
+});
+
+static RE_HREF: LazyLock = LazyLock::new(|| {
+ Regex::new(r#"href="([^"]+)""#).expect("Invalid HREF regex")
+});
+
+static RE_QUARTO_ATTR: LazyLock = LazyLock::new(|| {
+ Regex::new(r"\{[#.]?[a-zA-Z][^}]*\}").expect("Invalid QUARTO_ATTR regex")
+});
+
+static RE_FIG_ALT: LazyLock =
+ LazyLock::new(|| Regex::new(r"\{fig-[^}]+\}").expect("Invalid FIG_ALT regex"));
+
+static RE_EMPTY_P: LazyLock =
+ LazyLock::new(|| Regex::new(r"
+// These are empty placeholder divs that Observable JS runtime fills in
+static RE_OJS_CELL: LazyLock = LazyLock::new(|| {
+ Regex::new(r#"
\s*
"#)
+ .expect("Invalid OJS_CELL regex")
+});
+
+// Match video elements:
+// Ghost strips video tags, so we need to wrap them in HTML cards
+static RE_VIDEO: LazyLock = LazyLock::new(|| {
+ Regex::new(r#"(?s)"#).expect("Invalid VIDEO regex")
+});
+
+// Match collapsible code blocks: ...
+// Ghost strips details/summary, so we wrap in HTML cards to preserve toggle behavior
+static RE_CODE_FOLD: LazyLock = LazyLock::new(|| {
+ Regex::new(r#"(?s)]*class="[^"]*code-fold[^"]*"[^>]*>.*?"#)
+ .expect("Invalid CODE_FOLD regex")
+});
+
+// Match tags - captures the opening tag including all attributes
+static RE_IMG_TAG: LazyLock = LazyLock::new(|| {
+ Regex::new(r#"]*)/?>"#).expect("Invalid IMG_TAG regex")
+});
+
+// Match cell-output divs (Quarto code output blocks)
+// Ghost strips custom classes, so we wrap in HTML cards to preserve styling
+static RE_CELL_OUTPUT: LazyLock = LazyLock::new(|| {
+ Regex::new(r#"(?s)
]*class="[^"]*cell-output[^"]*"[^>]*>.*?
(?:\s*)?"#)
+ .expect("Invalid CELL_OUTPUT regex")
+});
+
+// =============================================================================
+// Syntax Highlighting via Code Injection
+// =============================================================================
+
+// Prism.js CDN injection for syntax highlighting
+// This gets injected into the post's via codeinjection_head
+// Includes a script to detect language and add classes since Ghost strips them
+// Also includes CSS to style code outputs differently from input code
+const PRISM_CODE_INJECTION: &str = r#"
+
+
+
+"#;
+
+// Plotly.js CDN injection for interactive charts
+const PLOTLY_CODE_INJECTION: &str =
+ r#""#;
+
+// Code annotation styling and interactivity
+// Provides CSS for annotated code blocks and JS for click-to-highlight behavior
+const ANNOTATION_CODE_INJECTION: &str = r#"
+"#;
+
+// CSS for code-fold toggle behavior (collapsible code blocks)
+const CODE_FOLD_CSS_INJECTION: &str = r#""#;
+
+/// Check if content has code blocks that would benefit from syntax highlighting
+fn has_code_blocks(content: &str) -> bool {
+ content.contains("
bool {
+ content.contains("class=\"code-fold\"") || content.contains("class='code-fold'")
+}
+
+/// Check if content has Quarto code annotations
+/// Looks for the code-annotation-anchor class which indicates annotated code
+fn has_code_annotations(content: &str) -> bool {
+ content.contains("code-annotation-anchor") || content.contains("code-annotation-container-grid")
+}
+
+/// Convert HTML content to mobiledoc format using an HTML card
+/// The HTML card preserves raw HTML without Ghost's usual sanitization
+fn html_to_mobiledoc(html: &str) -> String {
+ // Mobiledoc format with a single HTML card
+ // The HTML card is meant for "full HTML" content like interactive graphs
+ let mobiledoc = serde_json::json!({
+ "version": "0.3.1",
+ "markups": [],
+ "atoms": [],
+ "cards": [["html", {"html": html}]],
+ "sections": [[10, 0]]
+ });
+ mobiledoc.to_string()
+}
+
+/// Convert HTML content to mobiledoc format with native image cards for uploaded images
+/// This avoids Ghost stripping img tags from HTML cards
+fn html_to_mobiledoc_with_images(html: &str, images: &[UploadedImage]) -> String {
+ if images.is_empty() {
+ return html_to_mobiledoc(html);
+ }
+
+ // Split content at placeholder positions and build cards
+ let mut cards: Vec = Vec::new();
+ let mut sections: Vec = Vec::new();
+ let mut remaining = html.to_string();
+ let mut card_index = 0;
+
+ for image in images {
+ if let Some(pos) = remaining.find(&image.placeholder_id) {
+ // Add HTML card for content before the placeholder
+ let before = &remaining[..pos];
+ if !before.trim().is_empty() {
+ cards.push(serde_json::json!(["html", {"html": before}]));
+ sections.push(serde_json::json!([10, card_index]));
+ card_index += 1;
+ }
+
+ // Add image card using Ghost's native format
+ let mut image_card = serde_json::json!({
+ "src": image.url,
+ "alt": image.alt
+ });
+ if let Some(w) = image.width {
+ image_card["width"] = serde_json::json!(w);
+ }
+ if let Some(h) = image.height {
+ image_card["height"] = serde_json::json!(h);
+ }
+ cards.push(serde_json::json!(["image", image_card]));
+ sections.push(serde_json::json!([10, card_index]));
+ card_index += 1;
+
+ // Continue with remaining content after placeholder
+ remaining = remaining[pos + image.placeholder_id.len()..].to_string();
+ }
+ }
+
+ // Add remaining content as final HTML card
+ if !remaining.trim().is_empty() {
+ cards.push(serde_json::json!(["html", {"html": remaining}]));
+ sections.push(serde_json::json!([10, card_index]));
+ }
+
+ let mobiledoc = serde_json::json!({
+ "version": "0.3.1",
+ "markups": [],
+ "atoms": [],
+ "cards": cards,
+ "sections": sections
+ });
+ mobiledoc.to_string()
+}
+
+// =============================================================================
+// Data Types
+// =============================================================================
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
- iat: usize,
- exp: usize,
+ iat: i64,
+ exp: i64,
aud: String,
}
@@ -24,10 +633,17 @@ struct PostPayload {
struct Post {
title: String,
slug: String,
- html: String,
+ /// Content in mobiledoc format (HTML card wraps raw HTML)
+ mobiledoc: String,
+ /// Content in HTML format (for updating posts that use HTML)
+ #[serde(skip)]
+ html_content: String,
status: String,
published_at: String,
updated_at: String,
+ /// RSS entry's updated timestamp (for comparison, not sent to Ghost)
+ #[serde(skip)]
+ source_updated: Option>,
canonical_url: String,
tags: Vec,
feature_image: Option,
@@ -35,348 +651,2389 @@ struct Post {
feature_image_caption: Option,
meta_description: Option,
custom_excerpt: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ codeinjection_head: Option,
}
-impl Post {
- async fn new(entry: Entry) -> Post {
- let title = entry.title.as_ref().unwrap().content.clone();
-
- let link = entry.links.first().unwrap().href.as_str();
- let slug = get_slug(link);
-
- let summary = summarize_url(link).await;
-
- // Extract content from ghost-optimized version
- let ghost_content = extract_article_content(&link).await;
-
- let html = html! {
- div class="ghost-summary" {
- h3 { "Summary" }
- p { (summary) }
- }
- div class="ghost-content" {
- (maud::PreEscaped(ghost_content))
- }
- div class="ghost-footer" {
- hr {}
- p {
- em {
- "This content was originally posted on my projects website "
- a href=(link) { "here" }
- ". The above summary was generated by the "
- a href=("https://help.kagi.com/kagi/api/summarizer.html") {"Kagi Summarizer"}
- "."
- }
- }
- }
- }.into_string();
-
- let status = "published".to_owned();
-
- let published_at = entry.published.unwrap().to_rfc3339();
-
- let updated_at = chrono::Utc::now().to_rfc3339();
-
- let canonical_url = link.to_owned();
-
- let mut tags: Vec = entry
- .categories
- .iter()
- .map(|category| category.term.as_str().to_owned())
- .collect();
- tags.push("Projects Website".to_owned());
-
- // The rest of the data is optional and has to be pulled from the documents OpenGraph
- let raw_html = reqwest::get(link).await.unwrap().text().await.unwrap();
- let document = Html::parse_document(&raw_html);
- let selector = Selector::parse("meta[property^='og:']").unwrap();
-
- let mut feature_image = None;
- let mut feature_image_alt = None;
- let mut feature_image_caption = None;
- let mut meta_description = None;
- let mut custom_excerpt = None;
-
- for meta in document.select(&selector) {
- match meta.value().attr("property") {
- Some("og:image") => feature_image = meta.value().attr("content").map(String::from),
- Some("og:image:alt") => {
- // Ghost API limits this to 190 chars
- feature_image_alt = meta.value().attr("content").map(|desc| {
- desc.chars().take(190).collect()
- })
- }
- Some("og:image:description") => {
- feature_image_caption = meta.value().attr("content").map(String::from)
- }
- Some("og:description") => {
- meta_description = meta.value().attr("content").map(String::from);
-
- // Ghost API limits this to 300 chars
- custom_excerpt = meta.value().attr("content").map(|desc| {
- desc.chars().take(300).collect()
- });
- }
- _ => {}
- }
- }
-
-
- let x = Post {
- title,
- slug,
- html,
- status,
- published_at,
- updated_at,
- canonical_url,
- tags,
- feature_image,
- feature_image_alt,
- feature_image_caption,
- meta_description,
- custom_excerpt,
- };
- dbg!(&x);
- x
- }
-}
-
-fn get_slug(link: &str) -> String {
- link.split_once("/posts/").unwrap().1.trim_end_matches('/').to_string()
-}
-
-async fn extract_article_content(original_link: &str) -> String {
- // Convert original link to ghost-content version
- let ghost_link = original_link.replace("projects.ansonbiggs.com", "projects.ansonbiggs.com/ghost-content");
-
- match reqwest::get(&ghost_link).await {
- Ok(response) => {
- match response.text().await {
- Ok(html_content) => {
- let document = Html::parse_document(&html_content);
-
- // Try different selectors to find the main content
- let content_selectors = [
- "#quarto-content main",
- "#quarto-content",
- "main",
- "article",
- ".content",
- "body"
- ];
-
- for selector_str in &content_selectors {
- if let Ok(selector) = Selector::parse(selector_str) {
- if let Some(element) = document.select(&selector).next() {
- let content = element.inner_html();
-
- if !content.trim().is_empty() {
- return content;
- }
- }
- }
- }
-
- // Fallback: return original content with iframe if extraction fails
- format!(r#"
-
Content extraction failed. Falling back to embedded view: