├── .eslintrc.js
├── .gitignore
├── CHANGELOG.md
├── CODE_TAG
├── LICENSE
├── README.md
├── SConstruct
├── index.html
├── package.json
├── public
├── css
│ └── font-awesome.min.css
├── favicon.ico
└── fonts
│ ├── FontAwesome.otf
│ ├── fontawesome-webfont.eot
│ ├── fontawesome-webfont.svg
│ ├── fontawesome-webfont.ttf
│ ├── fontawesome-webfont.woff
│ ├── fontawesome-webfont.woff2
│ └── password.woff2
├── src
├── AccountAppearance.vue
├── AccountSettings.vue
├── AccountTeams.vue
├── AccountView.vue
├── App.vue
├── Award.vue
├── Button.vue
├── ChartsDialog.vue
├── ChartsView.vue
├── ClientVersion.vue
├── CommonSettings.vue
├── ConnectDialog.vue
├── Dialog.vue
├── DragList.vue
├── FAHLogo.vue
├── GPUFieldset.vue
├── GroupSettings.vue
├── HelpBalloon.vue
├── ImageInput.vue
├── InfoItem.vue
├── LogView.vue
├── LoginDialog.vue
├── MachineDetailsView.vue
├── MachineGroup.vue
├── MachineMux.vue
├── MachineView.vue
├── MachinesView.vue
├── MainHeader.vue
├── MainMenu.vue
├── MessageDialog.vue
├── NewAccountDialog.vue
├── NewsView.vue
├── Pacify.vue
├── PauseDialog.vue
├── PlotView.vue
├── ProgressBar.vue
├── ProjectView.vue
├── ProjectsView.vue
├── ResetView.vue
├── SettingsView.vue
├── StatsView.vue
├── TeamDialog.vue
├── UnitDetailsView.vue
├── UnitField.vue
├── UnitHeader.vue
├── UnitHeaders.vue
├── UnitInfo.vue
├── UnitsView.vue
├── VerifyView.vue
├── ViewHeader.vue
├── Visualization.vue
├── WUsView.vue
├── account.js
├── api-sock.js
├── api.js
├── base.styl
├── bip39.js
├── cache.js
├── chart.js
├── crypto.js
├── dark.styl
├── data-series.js
├── direct-mach-conn.js
├── light.styl
├── mach-connection.js
├── machine.js
├── machines.js
├── main.js
├── matrix.js
├── news.js
├── node-mach-conn.js
├── node.js
├── projects.js
├── router.js
├── sock.js
├── stats.js
├── subscriber.js
├── unit.js
├── unit_fields.json
├── updatable.js
├── util.js
└── viewer
│ ├── InfiniteGridHelper.js
│ ├── Sky.js
│ ├── grid.frag
│ ├── grid.vert
│ ├── sky.frag
│ └── sky.vert
└── vite.config.js
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | node: true,
4 | },
5 | extends: [
6 | 'eslint:recommended',
7 | 'plugin:vue/vue3-recommended',
8 | ],
9 | rules: {
10 | "vue/max-attributes-per-line": 'off',
11 | "max-len": ['error', { code: 120, ignoreUrls: true, ignoreComments: true }],
12 | "vue/html-closing-bracket-newline": ["error", {
13 | "singleline": "never",
14 | "multiline": "never"
15 | }],
16 | "vue/singleline-html-element-content-newline": 'off'
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | /dist
4 | /dist.txt
5 | /*.tar.bz2
6 | /*.zip
7 | *~
8 | /package-lock.json
9 | /.sconsign.dblite
10 | /.sconf_temp
11 | /config.log
12 | /.env.local
13 | /.env.*.local
14 | /crap
15 |
--------------------------------------------------------------------------------
/CODE_TAG:
--------------------------------------------------------------------------------
1 | This file is part of the Folding@home Client.
2 |
3 | The fah-client runs Folding@home protein folding simulations.
4 | Copyright (c) 2001-2024, foldingathome.org
5 | All rights reserved.
6 |
7 | This program is free software; you can redistribute it and/or modify
8 | it under the terms of the GNU General Public License as published by
9 | the Free Software Foundation; either version 3 of the License, or
10 | (at your option) any later version.
11 |
12 | This program is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | GNU General Public License for more details.
16 |
17 | You should have received a copy of the GNU General Public License along
18 | with this program; if not, write to the Free Software Foundation, Inc.,
19 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 |
21 | For information regarding this software email:
22 | Joseph Coffland
23 | joseph@cauldrondevelopment.com
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Folding@home Bastet Web Control
2 | ===============================
3 |
4 | This is the frontend Web app for the Folding@home v8 client, codenamed Bastet.
5 | See Also: https://github.com/FoldingAtHome/fah-client-bastet
6 |
7 | # Debian Linux Quick Start
8 |
9 | ## Get the code
10 |
11 | git clone https://github.com/foldingathome/fah-web-client-bastet
12 |
13 | ## Start the development web server
14 |
15 | cd fah-web-client-bastet
16 | npm i
17 | npm run dev
18 |
19 | ## Open the Browser
20 |
21 | With the development server running visit http://localhost:5173/
22 |
--------------------------------------------------------------------------------
/SConstruct:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | env = Environment(ENV = os.environ)
4 | try:
5 | paths = [os.environ.get('CBANG_HOME'), os.environ.get('CBANG_CONFIG_HOME')]
6 | env.Tool('config', toolpath = paths)
7 | except Exception as e:
8 | raise Exception('CBANG_HOME not set?\n' + str(e))
9 |
10 | env.CBLoadTools('dist packager')
11 | conf = env.CBConfigure()
12 |
13 | with open('package.json', 'r') as f: package_info = json.load(f)
14 |
15 | env.Replace(PACKAGE_VERSION = package_info['version'])
16 | env.Replace(dist_build = '')
17 | conf.Finish()
18 |
19 | if 'dist' in COMMAND_LINE_TARGETS:
20 | if not env.GetOption('clean'):
21 | env.RunCommandOrRaise(['npm', 'install'])
22 | env.RunCommandOrRaise(['npm', 'run', 'build'])
23 |
24 | distfiles = ['dist', 'LICENSE']
25 | tar = env.ZipDist(package_info['name'], distfiles)
26 | AlwaysBuild(tar)
27 | Alias('dist', tar)
28 | Clean(tar, ['dist', 'dist.txt'])
29 |
30 | if 'distclean' in COMMAND_LINE_TARGETS:
31 | Clean('distclean', ['.sconsign.dblite', '.sconf_temp', 'config.log',
32 | 'node_modules', 'package-lock.json',
33 | Glob('*.tar.bz2'), Glob('*.zip'),'dist', 'dist.txt'])
34 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Folding@home Client
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fah-web-control",
3 | "version": "8.5.3",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "vite build",
7 | "serve": "vite preview"
8 | },
9 | "dependencies": {
10 | "npm-check-updates": "^16.10.8",
11 | "pako": "^2.1.0",
12 | "three": "^0.151.3",
13 | "vue": "^3.2.47",
14 | "vue-router": "^4.1.6"
15 | },
16 | "devDependencies": {
17 | "@vitejs/plugin-vue": "^6.0.1",
18 | "@vue/compiler-sfc": "^3.2.47",
19 | "pug": "^3.0.2",
20 | "stylus": "^0.59.0",
21 | "vite": "^7.1.4"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FoldingAtHome/fah-web-client-bastet/67a738ea4785363d1a8cd913e4fd7323594e1e3c/public/favicon.ico
--------------------------------------------------------------------------------
/public/fonts/FontAwesome.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FoldingAtHome/fah-web-client-bastet/67a738ea4785363d1a8cd913e4fd7323594e1e3c/public/fonts/FontAwesome.otf
--------------------------------------------------------------------------------
/public/fonts/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FoldingAtHome/fah-web-client-bastet/67a738ea4785363d1a8cd913e4fd7323594e1e3c/public/fonts/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/public/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FoldingAtHome/fah-web-client-bastet/67a738ea4785363d1a8cd913e4fd7323594e1e3c/public/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/public/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FoldingAtHome/fah-web-client-bastet/67a738ea4785363d1a8cd913e4fd7323594e1e3c/public/fonts/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/public/fonts/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FoldingAtHome/fah-web-client-bastet/67a738ea4785363d1a8cd913e4fd7323594e1e3c/public/fonts/fontawesome-webfont.woff2
--------------------------------------------------------------------------------
/public/fonts/password.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FoldingAtHome/fah-web-client-bastet/67a738ea4785363d1a8cd913e4fd7323594e1e3c/public/fonts/password.woff2
--------------------------------------------------------------------------------
/src/AccountAppearance.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
64 |
65 |
66 | fieldset.settings.view-panel
67 | legend
68 | HelpBalloon(name="Appearance"): p.
69 | These settings change Web Control's appearance. Note, some
70 | settings are only available on wide screens.
71 |
72 | .setting.dark-setting
73 | HelpBalloon(name="Dark mode"): p Enables the dark mode theme.
74 | input(v-model="config.dark", type="checkbox")
75 |
76 | .setting.compact-setting
77 | HelpBalloon(name="Compact"): p Decrease gaps between display elements.
78 | input(v-model="config.compact", type="checkbox")
79 |
80 | .setting.wide-setting
81 | HelpBalloon(name="Wide Display"): p Use full screen width.
82 | input(v-model="config.wide", type="checkbox")
83 |
84 | .columns-setting
85 | HelpBalloon(name="Work Unit Columns"): p.
86 | Drag and drop columns to change their position and visibility.
87 | Note, only the default columns are displayed on small screens.
88 |
89 | .drag-zones
90 | .drag-zone
91 | Button.button-icon(icon="refresh", @click="reset_columns",
92 | title="Reset columns to default")
93 | DragList(:list="columns")
94 |
95 | .drag-zone(title="Move disabled columns here")
96 | .fa.fa-trash
97 | DragList(:list="unused_cols", :removable="false")
98 |
99 |
100 |
137 |
--------------------------------------------------------------------------------
/src/AccountSettings.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
83 |
84 |
85 | fieldset.settings.view-panel
86 | legend
87 | HelpBalloon(name="Account"): p.
88 | These settings affect all of the machines linked to your account.
89 |
90 | .setting
91 | HelpBalloon(name="Avatar")
92 | p An avatar is an optional image to represent you on Folding@home.
93 | p.
94 | The ideal avatar is 128x128 pixels.
95 | If you choose a larger image, it will be automatically scaled down.
96 | Your avatar's final size must not be less than 32 pixels in either
97 | dimension.
98 |
99 | div
100 | image-input(v-model="account.avatar", :width="128", :height="128",
101 | :min-width="32", :min-height="32", ref="avatar")
102 |
103 | CommonSettings(:config="account", ref="common")
104 |
105 | .setting
106 | HelpBalloon(name="Node"): p.
107 | A Folding@home node helps you connect to your remote clients.
108 | Unless otherwise instructed, it's best to leave the default value.
109 |
110 | input(v-model="account.node", :class="{error: !node_valid}")
111 |
112 | .setting
113 | HelpBalloon(name="Token")
114 | p.
115 | A token is used to connect your remote clients to your account. You
116 | can pass your account token to a client to cause it to link to your
117 | account. Once a machine is linked it will show up as one of your
118 | machines in your account.
119 |
120 | p.
121 | Anyone with your account token can link machines to your
122 | account. Once you've used an account token it's a good idea to
123 | generate a new one. Anytime you generate a new account token the
124 | previous account tokens are no longer valid. But any machines which
125 | are already linked to your account will remain linked.
126 |
127 | p.
128 | If a client is running locally on the same machine you've logged in
129 | to your Folding@home account on, it will automatically be linked to
130 | your account.
131 |
132 | input(v-model="$adata.token", :class="{password: !show.token}",
133 | readonly)
134 |
135 | .setting-actions
136 | Button.button-icon(:icon="'eye' + (show.token ? '' : '-slash')",
137 | @click="show.token = !show.token",
138 | :title="(show.token ? 'Hide' : 'Show') + ' account token'")
139 |
140 | Button.button-icon(icon="refresh", @click="reset_token",
141 | title="Generate a new account token")
142 |
143 | Button.button-icon(icon="copy", @click="copy_token",
144 | title="Copy account token to clipboard")
145 |
146 | .actions
147 | Button(@click="logout", icon="sign-out", text="Logout")
148 | Button.button-caution(@click="confirm_delete", text="Delete Account",
149 | icon="trash", title="Permanently delete this account")
150 |
151 |
152 |
159 |
--------------------------------------------------------------------------------
/src/AccountTeams.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
107 |
108 |
109 | TeamDialog(ref="team_dialog")
110 |
111 |
112 | fieldset.settings.view-panel.account-teams
113 | legend
114 | HelpBalloon(name="Teams")
115 | p Here you will find a list of teams you own.
116 | p You can create new teams or modify an existing team's settings.
117 | p Note, you can only delete teams which have not yet scored any points.
118 |
119 | div(v-if="!teams.length") You do not currently own any F@H teams.
120 | table.view-table(v-else)
121 | thead
122 | tr
123 | th.team-logo Logo
124 | th.team-id Team
125 | th.team-name Name
126 | th.team-actions Actions
127 |
128 | tbody
129 | tr(v-for="team in teams")
130 | td.team-logo: img(:src="team.logo")
131 |
132 | td.team-id
133 | a(:href="`${$stats.url}/team/${team.team}`", target="_blank")
134 | | {{team.team}}
135 |
136 | td.team-name: component(:href="team.url", target="_blank",
137 | :is="team.url ? 'a' : 'span'") {{team.name}}
138 |
139 | td.team-actions
140 | div
141 | Button.button-icon(icon="trash", @click="delete_team(team)",
142 | title="Delete team.", :disabled="!!team.wus")
143 | Button.button-icon(icon="pencil", @click="edit_team(team)",
144 | title="Edit team settings.")
145 |
146 | .actions
147 | Button(text="New Team", icon="plus", @click="create_team",
148 | title="Create a new Folding@home team.")
149 |
150 |
151 |
174 |
--------------------------------------------------------------------------------
/src/Award.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
71 |
72 |
73 | .award
74 | Button(@click="open()", icon="trophy", :text="title", :disabled="disabled")
75 |
76 | Dialog.award-dialog(ref="dialog", :buttons="[]", :header="title",
77 | width="auto", allowClickAway)
78 | template(v-slot:body)
79 | h2(v-if="!loaded") Loading...
80 | a(:href="url"): img(v-if="active", :src="url", @load="loaded = true")
81 |
82 |
83 |
100 |
--------------------------------------------------------------------------------
/src/Button.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
115 |
116 |
117 | a.button(@click="click", :href="link", :target="href ? '_blank' : ''",
118 | :class="_class")
119 | .fa(v-if="_icon", :class="'fa-' + _icon")
120 | img(v-if="image", :src="image")
121 | span.button-content(v-if="content") {{content}}
122 |
123 |
124 |
196 |
--------------------------------------------------------------------------------
/src/ChartsDialog.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
41 |
42 |
43 | Dialog.charts-dialog(ref="dialog", :buttons="['Ok']", width="90vw",
44 | height="90vh")
45 | template(v-slot:header) Team {{chart_mode}} Chart
46 | template(v-slot:body)
47 | .chart-controls
48 | .chart-modes
49 | label Mode:
50 | template(v-for="mode in ['PPD', 'Points', 'WUs']")
51 | input(type="radio", :id="'chart-mode-' + mode", :value="mode",
52 | v-model="$root.chart_mode")
53 | label(:for="'chart-mode-' + mode") {{mode}}
54 |
55 | .chart-sources
56 | label Source:
57 | template(v-for="source in ['Team', 'User', 'Both']")
58 | input(type="radio", :id="'chart-source-' + source",
59 | :value="source", v-model="$root.chart_source")
60 | label(:for="'chart-source-' + source") {{source}}
61 |
62 | charts-view(ref="chart", :charts="charts", :mode="$root.chart_mode",
63 | :source="$root.chart_source")
64 |
65 |
66 |
95 |
--------------------------------------------------------------------------------
/src/ClientVersion.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
42 |
43 |
44 | .client-version(v-if="version")
45 | a.outdated(v-if="outdated", :href="download_url", target="_blank",
46 | title="Client version outdated. Click to open download page")
47 | | #[.fa.fa-exclamation-triangle] v{{version}}
48 | |
49 | | #[.fa.fa-exclamation-triangle]
50 |
51 | span(v-else, :title="'Folding@home client version ' + version")
52 | | v{{version}}
53 |
54 |
55 |
66 |
--------------------------------------------------------------------------------
/src/CommonSettings.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
56 |
57 |
58 | .setting
59 | HelpBalloon(name="Username")
60 | p Choose a display name for your Folding@home account.
61 | p.
62 | The name does not have to be unique. It may contain any characters other
63 | than #[tt <], #[tt >], #[tt ;], or #[tt &] and must be
64 | between 2 and 100 characters in length.
65 |
66 | p If you wish to remain anonymous enter #[tt Anonymous].
67 |
68 | input(v-model="config.user", :class="{error: !user_valid}")
69 |
70 | .setting
71 | HelpBalloon(name="Team")
72 | p.
73 | You may wish to join a Folding@home team. If you do not already have
74 | a team you can create a new one via your F@H account settings.
75 |
76 | p Enter #[tt 0] for no team.
77 |
78 | input(v-model.number="config.team", :class="{error: !team_valid}",
79 | type="number")
80 |
81 | .setting
82 | HelpBalloon(name="Passkey")
83 | p.
84 | A passkey allows you to collect bonus points. Enter a passkey if you
85 | have one, otherwise leave this field blank.
86 |
87 | input(v-model="config.passkey", pattern="[\\da-fA-F]{31,32}",
88 | :class="{password: !show_key, error: !passkey_valid}")
89 |
90 | .setting-actions
91 | Button.button-icon(:icon="'eye' + (show_key ? '' : '-slash')",
92 | @click="show_key = !show_key",
93 | :title="(show_key ? 'Hide' : 'Show') + ' passkey'")
94 |
95 | .setting
96 | HelpBalloon(name="Cause"): p.
97 | You may choose a preferred cause to support. Folding@home will try to
98 | assign you more work supporting your preferred cause.
99 |
100 | select(v-model="config.cause")
101 | option(v-for="name in causes", :value="name") {{name}}
102 |
103 |
104 |
106 |
--------------------------------------------------------------------------------
/src/ConnectDialog.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
54 |
55 |
56 | Dialog.connect-dialog(:buttons="buttons", ref="dialog")
57 | template(v-slot:header) Connect directly to a client
58 | template(v-slot:body)
59 | label Address
60 | input(v-model="address")
61 |
62 |
63 |
69 |
--------------------------------------------------------------------------------
/src/Dialog.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
115 |
116 |
117 | Teleport(to="body")
118 | .dialog-overlay(ref="dialog" v-show="active", @click="click_away",
119 | :style="style")
120 | .dialog(:class="class", @click.stop="true")
121 | .dialog-header
122 | .dialog-header-slot.header-title: slot(name="header") {{header}}
123 | Button.dialog-close.button-icon(v-if="allowCancel",
124 | @click="close('cancel')", icon="times")
125 |
126 | .dialog-body
127 | slot(name="body")
128 |
129 | .dialog-footer
130 | Button(v-for="b in _buttons", @click="close(b.name)", v-bind="b")
131 |
132 |
133 |
183 |
--------------------------------------------------------------------------------
/src/FAHLogo.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 | .fah-logo
31 | a(href="https://foldingathome.org/", target="_blank")
32 | div FOLDING
33 | div #[span @]HOME
34 |
35 |
36 |
61 |
--------------------------------------------------------------------------------
/src/GPUFieldset.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
54 |
55 |
56 | fieldset.view-panel.gpu-fieldset
57 | legend {{id}}
58 | .info-group
59 | info-item(label="Description", :content="gpu.description")
60 | info-item(label="Vendor", :content="gpu.type")
61 |
62 | .info-group
63 | info-item(label="Supported", :content="gpu.supported", bool)
64 | info-item(label="UUID", :content="gpu.uuid")
65 |
66 | .info-group
67 | info-item(label="PCI Device ID", :content="'0x' + gpu.device.toString(16)")
68 | info-item(label="PCI Vendor ID", :content="'0x' + gpu.vendor.toString(16)")
69 |
70 | .info-group(v-for="dev of devs")
71 | info-item(:label="dev.name",
72 | :content="(dev.supported ? '' : 'un') + 'supported'")
73 |
74 | template(v-if="dev.compute")
75 | info-item(label="Compute", :content="dev.compute")
76 | info-item(label="Driver", :content="dev.driver")
77 |
78 |
79 |
81 |
--------------------------------------------------------------------------------
/src/HelpBalloon.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
42 |
43 |
44 | label.help-balloon(@click="active = !active")
45 | .help-overlay(v-show="active", @click.stop="active = false")
46 | .help-name {{name}}#[.fa.fa-question-circle]
47 | span(v-if="active")
48 | .fa.fa-caret-left
49 | .help-content.view-panel(@click.stop="true")
50 | .help-header
51 | h2.help-title {{name}} Help
52 | Button.button-icon(icon="times", @click="active=false")
53 | slot
54 |
55 |
56 |
132 |
--------------------------------------------------------------------------------
/src/ImageInput.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
149 |
150 |
151 | .image-input(:class="{error: !valid}")
152 | label(:for="input_id", title="Click to change image.")
153 | .fa.fa-times(@click.prevent="clear", title="Remove image.",
154 | v-if="this.modelValue")
155 | input(ref="input", type="file", accept="image/*", @change="on_change",
156 | :id="input_id")
157 |
158 |
159 |
160 |
193 |
--------------------------------------------------------------------------------
/src/InfoItem.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
38 |
39 |
40 | .info-item
41 | label {{label}}
42 | span {{bool ? (content ? 'true' : 'false') : content}}
43 |
44 |
45 |
79 |
--------------------------------------------------------------------------------
/src/MachineDetailsView.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
43 |
44 |
45 | .machine-details-view.page-view
46 | ViewHeader(title="Machine Details", :subtitle="mach.get_name()")
47 |
48 | .view-body(v-if="info")
49 | fieldset.view-panel
50 | legend Machine
51 |
52 | .info-group
53 | info-item(label="Hostname", :content="info.hostname")
54 | info-item(label="OS", :content="info.os")
55 |
56 | .info-group
57 | info-item(label="Client Version", :content="info.version")
58 | info-item(label="OS Version", :content="info.os_version")
59 |
60 | .info-group
61 | info-item(label="Build Mode", :content="info.mode")
62 | info-item(label="Revision", :content="info.revision")
63 |
64 | .info-group
65 | info-item(label="Has Battery", :content="info.has_battery", bool)
66 | info-item(label="On Battery", :content="info.on_battery", bool)
67 |
68 | fieldset.view-panel
69 | legend CPU
70 |
71 | .info-group
72 | info-item(label="Description", :content="info.cpu_brand")
73 |
74 | .info-group
75 | info-item(label="Cores", :content="info.cpus")
76 | info-item(label="Type", :content="info.cpu")
77 |
78 | GPUFieldset(v-for="(gpu, id) in info.gpus", :gpu="gpu", :id="id")
79 |
80 |
81 |
87 |
--------------------------------------------------------------------------------
/src/MachineGroup.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
72 |
73 |
74 | .machine-group-header(v-if="header",
75 | :style="{'grid-column': `span ${columns.length + 1}`}",
76 | :class="{error: !!failed, warn: warn}")
77 | .group-name.header-subtitle
78 | | {{group ? `Group ${group}` : 'Default Group'}}
79 |
80 | .group-resources(:title="mach.get_resources(group)")
81 | | {{mach.get_resources(group, 50)}}
82 |
83 | .group-status
84 | span(v-for="t in status", :title="t[1]", v-html="t[0]")
85 |
86 | .machine-group-actions
87 | Button.button-icon(v-if="mach.is_paused(group)", icon="play",
88 | @click="$emit('fold')", :disabled="!connected",
89 | title="Start folding in this group")
90 |
91 | Button.button-icon(v-else, @click="$emit('pause')", icon="pause",
92 | title="Pause folding in this group", :disabled="!connected")
93 |
94 | UnitsView(:units="units", :columns="columns", v-slot="{unit}")
95 | Button.button-icon(:disabled="!unit.paused || !connected",
96 | @click="confirm_dump(mach, unit)", icon="trash", title="Dump Work Unit")
97 |
98 | Button.button-icon(:disabled="!connected",
99 | :route="`${mach.get_url('/log')}?q=:WU${unit.number}:`",
100 | icon="list-alt", title="View Work Unit log")
101 |
102 | Button.button-icon(:route="'/unit/' + unit.id",
103 | icon="info-circle", :disabled="!unit.wu",
104 | title="View Work Unit details")
105 |
106 | Button.button-icon(
107 | :route="mach.get_url('/view/' + unit.id)", icon="eye",
108 | :disabled="!unit.wu || !connected", title="View 3D protein")
109 |
110 |
111 |
146 |
--------------------------------------------------------------------------------
/src/MachineMux.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
43 |
44 |
45 | router-view(v-if="mach", :mach="mach")
46 |
47 |
48 |
50 |
--------------------------------------------------------------------------------
/src/MachinesView.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
54 |
55 |
56 | .machines-view.page-view
57 | MainHeader
58 |
59 | .view-body
60 | .machines-view-header.view-panel
61 | .no-data(v-if="$machs.is_empty")
62 | p No folding machines found.
63 | p Login or install the Folding@home client software.
64 | p.
65 | If you are using Brave browser, please use "Shields Down" for
66 | this site.
67 |
68 | template(v-else)
69 |
70 |
71 | table.machines-info.view-table
72 | thead
73 | tr
74 | th Total PPD
75 | th Actions
76 |
77 | tbody
78 | tr
79 | td(title="Current total estimated Points Per Day")
80 | | {{$machs.ppd.toLocaleString()}}
81 | td
82 | .machines-actions
83 | Button(text="Fold All",
84 | @click="$root.fold()", success, icon="play",
85 | :disabled="$machs.is_empty",
86 | title="Start folding on all machines")
87 |
88 | Button(text="Pause All",
89 | @click="$root.pause()", icon="pause",
90 | :disabled="$machs.is_empty",
91 | title="Pause folding on all machines")
92 |
93 | template(v-for="mach in machs")
94 | MachineView(v-if="!mach.is_hidden()", :mach="mach")
95 |
96 |
97 |
129 |
--------------------------------------------------------------------------------
/src/MainHeader.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
38 |
39 |
40 | ViewHeader.main-header
41 | template(v-slot:center): slot(name="center")
42 |
43 | template(v-slot:actions)
44 | Button.button-image(v-if="$adata.avatar", route="/account/",
45 | :image="$adata.avatar",
46 | :title="$adata.user + ': Account Settings and Logout'")
47 |
48 | Button.button-icon(v-else-if="$account.logged_in", route="/account/",
49 | icon="user", :title="$adata.user + ': Account Settings and Logout'")
50 |
51 | Button(v-else, text="Login", icon="sign-in", @click="$root.login()",
52 | title="Login to Folding@home or register a new account")
53 |
54 | template(v-slot:menu)
55 | MainMenu
56 |
57 |
58 |
63 |
--------------------------------------------------------------------------------
/src/MainMenu.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
34 |
35 |
36 | .view-menu
37 | router-link(to="/machines") Machines
38 | router-link(to="/wus") Work Units
39 | router-link(to="/stats") Stats
40 | router-link(to="/projects") Projects
41 | router-link(to="/news") News
42 |
43 |
44 |
72 |
--------------------------------------------------------------------------------
/src/MessageDialog.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
88 |
89 |
90 | Dialog.message-dialog(:buttons="buttons", ref="dialog", :allowCancel="false")
91 | template(v-slot:header)
92 | .fa(v-if="icon", :class="'fa-' + icon")
93 | span(v-html="title")
94 |
95 | template(v-slot:body): div(v-html="body")
96 |
97 |
98 |
104 |
--------------------------------------------------------------------------------
/src/NewAccountDialog.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
72 |
73 |
74 | Dialog(:buttons="buttons", ref="dialog", :allowCancel="false")
75 | template(v-slot:header) Create New Folding@home Account
76 | template(v-slot:body)
77 | .new-account
78 | fieldset.view-panel
79 | legend Required
80 |
81 | .setting
82 | label Username
83 | input(ref="user", v-model="account.user", minLength="2",
84 | maxLength="100", required)
85 |
86 | fieldset.view-panel
87 | legend Optional
88 |
89 | .setting
90 | label Team
91 | input(ref="team", v-model.number="account.team", type="number",
92 | min="0")
93 |
94 | .setting
95 | label Passkey
96 | input(ref="passkey", v-model="account.passkey",
97 | pattern="[\\da-fA-F]{31,32}")
98 |
99 |
100 |
120 |
--------------------------------------------------------------------------------
/src/NewsView.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
39 |
40 |
41 | .news-view.page-view
42 | MainHeader
43 |
44 | .view-body
45 | .header-title(v-if="!feed.length") Loading...
46 |
47 | article.view-panel(v-for="item in feed")
48 | .article-image
49 | a(:href="item.url", target="_blank"): img(:src="item.image")
50 |
51 | .article-content
52 | .article-title
53 | a(:href="item.url", target="_blank")
54 | .header-title(v-html="item.title")
55 |
56 | .article-byline
57 | | By #[span.author {{item.author}}] on #[span.date {{item.date}}].
58 |
59 | .article-body(v-html="item.description")
60 |
61 |
62 |
97 |
--------------------------------------------------------------------------------
/src/Pacify.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
51 |
52 |
53 | Teleport(to="body")
54 | .pacify-overlay(v-show="active")
55 | .fa.fa-spinner.fa-pulse
56 |
57 |
58 |
73 |
--------------------------------------------------------------------------------
/src/PauseDialog.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
46 |
47 |
48 | Dialog(:buttons="buttons", ref="dialog")
49 | template(v-slot:header) Resume, Pause or Finish
50 | template(v-slot:body).
51 | Would you like to abort "finishing" and resume continuous folding,
52 | pause folding now or finish all the active work units then pause?
53 |
54 |
55 |
57 |
--------------------------------------------------------------------------------
/src/PlotView.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
133 |
134 |
135 | canvas.plot-view
136 |
137 |
138 |
142 |
--------------------------------------------------------------------------------
/src/ProgressBar.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
34 |
35 |
36 | .progress-bar
37 | .progress(:style="{width: progress + '%'}")
38 | .progress-text {{progress}}%
39 |
40 |
41 |
65 |
--------------------------------------------------------------------------------
/src/ProjectView.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
58 |
59 |
60 | fieldset.project.view-panel(@click="toggle")
61 | legend Project {{project.id}}
62 |
63 | .project-title
64 | .project-byline By {{project.manager}}, {{project.institution}}
65 | .project-cause.header-subtitle Target: {{project.cause}}
66 |
67 | .project-body(:class="{'read-less': !more}")
68 | .project-details
69 | .project-image
70 | img(v-if="project.thumb", :src="img_url + project.thumb")
71 | .project-description(v-html="project.description")
72 |
73 | .project-manager
74 | .project-manager-image
75 | img(v-if="project.mthumb", :src="img_url + project.mthumb")
76 | .project-manager-description(v-html="project.mdescription")
77 |
78 | .project-footer(v-if="!full")
79 | .read-more(@click.stop="more = !more") {{more ? '- Collapse' : '+ Expand'}}
80 |
81 |
82 |
117 |
--------------------------------------------------------------------------------
/src/ProjectsView.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
34 |
35 |
36 | .projects-view.page-view
37 | MainHeader
38 |
39 | .view-body
40 | h2(v-if="$projects.is_loading() && !this.projects.length") Loading...
41 |
42 | template(v-else-if="!projects.length")
43 | p No active projects.
44 | p While you are folding active projects will display here.
45 |
46 | ProjectView(v-for="project in projects", :project="project")
47 |
48 |
49 |
58 |
--------------------------------------------------------------------------------
/src/ResetView.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
67 |
68 |
69 | .reset-view.page-view
70 | ViewHeader(title="Reset your Folding@home account")
71 | template(v-slot:actions)
72 | Button(icon="sign-in", text="Login", success,
73 | @click="$root.login()", title="Login to your Folding@home account",
74 | :disabled="!success")
75 |
76 | .view-body
77 | p(v-if="success").
78 | Account reset successful. You may now login.
79 |
80 | template(v-else)
81 | p.
82 | Resetting your Folding@home account passphrase will have the following
83 | effects:
84 |
85 | ul
86 | li Any remote clients will need to be reconnected to your account.
87 | li Any other logins to this account will need to relogin.
88 | li Your Folding@home account settings and points will stay the same.
89 |
90 | form(v-if="!success")
91 | fieldset.settings
92 | .setting
93 | label Passphrase
94 | input(v-model="passphrase", :type="show ? 'text' : 'password'",
95 | name="password", autocomplete="current-password")
96 |
97 | .setting-actions
98 | Button.button-icon(:icon="'eye' + (show ? '' : '-slash')",
99 | @click="show = !show",
100 | :title="(show ? 'Hide' : 'Show') + ' passphrase'")
101 |
102 | .setting
103 | label Confirm Passphrase
104 | input(v-model="passphrase2", :type="show ? 'text' : 'password'",
105 | autocomplete="new-password")
106 |
107 | .setting-actions
108 | Button.button-icon(icon="refresh", @click="generate_passphrase",
109 | title="Generate a memorable and strong random passphrase")
110 |
111 | Button.button-icon(icon="copy", @click="copy_passphrase",
112 | title="Copy passphrase to clipboard")
113 |
114 | .actions
115 | Button(icon="check", text="Reset", @click="reset", success,
116 | :disabled="!valid", title="Reset your Folding@home account")
117 |
118 |
119 |
143 |
--------------------------------------------------------------------------------
/src/TeamDialog.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
79 |
80 |
81 | Dialog.team-dialog(:buttons="buttons", ref="dialog")
82 | template(v-slot:header) {{create ? 'Create' : 'Edit'}} Team
83 | template(v-slot:body)
84 | fieldset.settings
85 | .setting
86 | HelpBalloon(name="Name")
87 | p Choose a display name for your Folding@home team.
88 | p.
89 | The name may contain any characters other than #[tt <],
90 | #[tt >], #[tt ;], or #[tt &] and must be between 2 and
91 | 100 characters in length.
92 | input(v-model="team.name", :class="{error: !name_valid}")
93 |
94 | .setting
95 | HelpBalloon(name="URL")
96 | p.
97 | Optionally set a URL for your team. This could be your team's
98 | Facebook or other home page.
99 | p It must be a valid URL or empty.
100 |
101 | input(v-model="team.url", :class="{error: !url_valid}",
102 | @change="fix_url")
103 |
104 | .setting
105 | HelpBalloon(name="Logo")
106 | p Optionally set your team's logo.
107 | p.
108 | The ideal logo is 128x128 pixels.
109 | If you choose a larger image, it will be automatically scaled down.
110 | Your logo's final size must not be less than 32 pixels in either
111 | dimension.
112 |
113 | image-input(v-model="team.logo", :width="128", :height="128",
114 | :min-width="32", :min-height="32", ref="logo")
115 |
116 |
117 |
122 |
--------------------------------------------------------------------------------
/src/UnitDetailsView.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
71 |
72 |
73 | .unit-details-view.page-view
74 | ViewHeader(title="Work Unit Details")
75 |
76 | .view-body(v-if="unit.wu")
77 | fieldset.view-panel
78 | legend(:title="`Unit ID ${unit.id}`") Work Unit {{'#' + unit.number}}
79 |
80 | .info-group
81 | unit-info(:unit="unit", field="Status Text")
82 | unit-info(:unit="unit", field="Progress")
83 |
84 | .info-group
85 | unit-info(:unit="unit", field="Machine")
86 | unit-info(:unit="unit", field="OS Text")
87 |
88 | .info-group
89 | unit-info(:unit="unit", field="TPF")
90 | unit-info(:unit="unit", field="PPD")
91 |
92 | .info-group
93 | unit-info(:unit="unit", field="CPUs")
94 | unit-info(:unit="unit", field="GPUs Text")
95 |
96 | .info-group
97 | unit-info(:unit="unit", field="Run Time")
98 | unit-info(:unit="unit", field="ETA")
99 |
100 | .info-group
101 | unit-info(:unit="unit", field="Assign Time")
102 | unit-info(:unit="unit", field="Base Credit")
103 |
104 | .info-group
105 | unit-info(:unit="unit", field="Deadline")
106 | unit-info(:unit="unit", field="Timeout")
107 |
108 | .info-group
109 | unit-info(:unit="unit", field="Core")
110 | unit-info(:unit="unit", field="Work Server")
111 |
112 | .info-group
113 | unit-info(:unit="unit", field="Project")
114 | unit-info(:unit="unit", field="RCG")
115 |
116 | fieldset.view-panel
117 | legend Logged Credits
118 |
119 | div(v-if="loading") Loading...
120 | div(v-else-if="!credits.length") No credits logged
121 | template(v-else)
122 | table.view-table
123 | thead
124 | tr
125 | th Code
126 | th User
127 | th Team
128 | th Credit
129 | th Assigned
130 | th Credited
131 |
132 | tbody
133 | tr(v-for="credit in credits")
134 | td.code {{credit.code}}
135 | td.user {{credit.user}}
136 | td.team {{credit.team}}
137 | td.credit {{(credit.credit || 0).toLocaleString()}}
138 | td.assigned {{credit.assign_time}}
139 | td.credited {{credit.credit_time}}
140 |
141 | p May include credits awarded to other users.
142 |
143 | ProjectView(v-if="project.description", :project="project", :full="true")
144 |
145 |
146 |
154 |
--------------------------------------------------------------------------------
/src/UnitField.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
35 |
36 |
37 | .unit-field(:class="unit.get_field_class(field, odd)",
38 | :title="unit.get_field_title(field)")
39 | div(v-if="field.toLowerCase() == 'progress'")
40 | ProgressBar(:progress="unit.progress")
41 | div(v-else, v-html="unit.get_field_content(field)")
42 |
43 |
44 |
75 |
--------------------------------------------------------------------------------
/src/UnitHeader.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
39 |
40 |
41 | .unit-header(:class="Unit.get_field_header_class(field)",
42 | :title="Unit.get_field_desc(field)") {{Unit.get_field_header(field)}}
43 |
44 |
45 |
47 |
--------------------------------------------------------------------------------
/src/UnitHeaders.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
39 |
40 |
41 | template(v-for="col in columns")
42 | unit-header(v-if="Unit.has_field(col)", :field="col")
43 |
44 | .unit-header.unit-actions
45 | slot
46 |
47 |
48 |
50 |
--------------------------------------------------------------------------------
/src/UnitInfo.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
35 |
36 |
37 | .info-item
38 | label: unit-header(:field="field")
39 | span: unit-field(:unit="unit", :field="field")
40 |
41 |
42 |
44 |
--------------------------------------------------------------------------------
/src/UnitsView.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
42 |
43 |
44 | template(v-for="(unit, index) in units", :key="unit.id")
45 | template(v-for="col in columns")
46 | UnitField(v-if="Unit.has_field(col)", :unit="unit", :field="col",
47 | :odd="index & 1")
48 |
49 | .unit-field.unit-actions(:class="`row-${index & 1 ? 'odd' : 'even'}`")
50 | slot(:unit="unit")
51 |
52 |
53 |
85 |
--------------------------------------------------------------------------------
/src/VerifyView.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
47 |
48 |
49 | .verify-view.page-view
50 | ViewHeader(title="Verifying Account Email")
51 | template(v-slot:actions)
52 | Button(icon="sign-in", text="Login", success,
53 | @click="$root.login()", title="Login to your Folding@home account",
54 | :disabled="!success && !failed")
55 |
56 | .view-body
57 | div(v-if="!success && !failed")
58 | p Verifying...
59 |
60 | div(v-if="success")
61 | p Email verification successful.
62 |
63 | div(v-if="failed")
64 | p Email verification failed.
65 | p.
66 | A verification token can be used only once. If you've already
67 | verified your email address, please try to login.
68 |
69 |
70 |
71 |
75 |
--------------------------------------------------------------------------------
/src/ViewHeader.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
76 |
77 |
78 | .view-header
79 | .header-top
80 | template(v-if="team.team")
81 | component.active-team(:class="{'active-team-link': !!team.url}",
82 | :is="team.url ? 'a' : 'div'", target="_blank", :href="team.url",
83 | :title="team.url ? `Visit your team's home page.` : ''",
84 | @click.prevent="confirm_team_url(team.url)")
85 | img.team-logo(:src="team.logo")
86 | .team-name.header-title {{team.name}}
87 |
88 | charts-view(v-if="!title", ref="chart", :charts="$stats.charts",
89 | :config="chart_config", @activate="on_chart_activate",
90 | :mode="$root.chart_mode", :source="$root.chart_source")
91 |
92 | FAHLogo(v-else)
93 |
94 | .header-center
95 | slot(name="center")
96 | .header-title(v-if="title") {{title}}
97 | .header-subtitle(v-if="subtitle") {{subtitle}}
98 |
99 | .header-actions
100 | slot(name="actions")
101 | Button(text="Close", icon="times", @click="close")
102 |
103 | .header-menu
104 | slot(name="menu")
105 |
106 |
107 |
188 |
--------------------------------------------------------------------------------
/src/api-sock.js:
--------------------------------------------------------------------------------
1 | /******************************************************************************\
2 |
3 | This file is part of the Folding@home Client.
4 |
5 | The fah-client runs Folding@home protein folding simulations.
6 | Copyright (c) 2001-2024, foldingathome.org
7 | All rights reserved.
8 |
9 | This program is free software; you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation; either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License along
20 | with this program; if not, write to the Free Software Foundation, Inc.,
21 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 |
23 | For information regarding this software 358,797,681email:
24 | Joseph Coffland
25 | joseph@cauldrondevelopment.com
26 |
27 | \******************************************************************************/
28 |
29 | import Sock from './sock.js'
30 | import Subscriber from './subscriber.js'
31 |
32 |
33 | class TeamSubscriber extends Subscriber {
34 | constructor(sock, team, max_count = 10000) {
35 | super(sock, max_count)
36 | this.ref = `team-${team}`
37 | this.msg = {timeseries: 'team.score', team, '$ref': this.ref}
38 | }
39 | }
40 |
41 |
42 | class UserSubscriber extends Subscriber {
43 | constructor(sock, uid, pid, max_count = 10000) {
44 | super(sock, max_count)
45 | this.ref = `user-${uid}-${pid}`
46 | this.msg = {timeseries: 'user.score', uid, pid, '$ref': this.ref}
47 | }
48 | }
49 |
50 |
51 | function get_chart_subscriber(sock, chart) {
52 | switch (chart.type) {
53 | case 'team': return new TeamSubscriber(sock, chart.team)
54 | case 'user': return new UserSubscriber(sock, chart.uid, chart.pid)
55 | }
56 | }
57 |
58 |
59 | class APISock extends Sock {
60 | constructor(ctx, ...args) {
61 | super(...args)
62 | this.ctx = ctx
63 | this.subs = {}
64 | this.nextID = 1
65 | }
66 |
67 |
68 | subscribe(chart, cb) {
69 | let sub = get_chart_subscriber(this, chart)
70 | let ref = sub.ref
71 |
72 | if (this.subs[ref] == undefined) this.subs[ref] = sub
73 |
74 | return {ref, id: this.subs[ref].add_subscriber(cb)}
75 | }
76 |
77 |
78 | unsubscribe(o) {this.subs[o.ref].del_subscriber(o.id)}
79 |
80 |
81 | on_message(msg) {
82 | if (msg.data != undefined && msg.data.message != undefined) {
83 | console.error(msg.data.message)
84 | console.debug(msg)
85 | return
86 | }
87 |
88 | let sub = this.subs[msg.$ref]
89 | if (sub != undefined) sub.on_message(msg)
90 |
91 | else throw 'Unsupported API Websocket message: ' + JSON.stringify(msg)
92 | }
93 |
94 |
95 | on_open(event) {Object.values(this.subs).map(t => t.on_open(event))}
96 |
97 |
98 | on_close(event) {
99 | setTimeout(() => this.connect(), 1000)
100 | Object.values(this.subs).map(t => t.on_close(event))
101 | }
102 |
103 |
104 | on_error(event) {console.debug('APISock error', event)}
105 | }
106 |
107 |
108 | export default APISock
109 |
--------------------------------------------------------------------------------
/src/cache.js:
--------------------------------------------------------------------------------
1 | /******************************************************************************\
2 |
3 | This file is part of the Folding@home Client.
4 |
5 | The fah-client runs Folding@home protein folding simulations.
6 | Copyright (c) 2001-2024, foldingathome.org
7 | All rights reserved.
8 |
9 | This program is free software; you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation; either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License along
20 | with this program; if not, write to the Free Software Foundation, Inc.,
21 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 |
23 | For information regarding this software email:
24 | Joseph Coffland
25 | joseph@cauldrondevelopment.com
26 |
27 | \******************************************************************************/
28 |
29 | class Cache {
30 | constructor(name, timeout) {
31 | this.name = name
32 | this.timeout = timeout
33 | }
34 |
35 |
36 | async set(key, value, status) {
37 | let data = {ts: new Date().toISOString(), value, status}
38 |
39 | try {
40 | if (!this.cache) this.cache = await caches.open(this.name)
41 | await this.cache.put(key, new Response(JSON.stringify(data)))
42 |
43 | } catch (e) {
44 | if (!this._cache) this._cache = {}
45 | this._cache[key] = data
46 | }
47 | }
48 |
49 |
50 | async get(key, timeout, withStatus = false) {
51 | let data
52 | if (timeout == undefined) timeout = this.timeout
53 |
54 | try {
55 | if (!this.cache) this.cache = await caches.open(this.name)
56 |
57 | let res = await this.cache.match(key)
58 | if (!res) return
59 | data = await res.json()
60 |
61 | } catch (e) {
62 | if (!this._cache) this._cache = {}
63 | data = this._cache[key]
64 | }
65 |
66 | if (data &&
67 | (!timeout || Date.now() - new Date(data.ts).getTime() < timeout))
68 | return withStatus ? data : data.value
69 | }
70 | }
71 |
72 |
73 | export default Cache
74 |
--------------------------------------------------------------------------------
/src/dark.styl:
--------------------------------------------------------------------------------
1 | /******************************************************************************\
2 |
3 | This file is part of the Folding@home Client.
4 |
5 | The fah-client runs Folding@home protein folding simulations.
6 | Copyright (c) 2001-2024, foldingathome.org
7 | All rights reserved.
8 |
9 | This program is free software; you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation; either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License along
20 | with this program; if not, write to the Free Software Foundation, Inc.,
21 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 |
23 | For information regarding this software email:
24 | Joseph Coffland
25 | joseph@cauldrondevelopment.com
26 |
27 | \******************************************************************************/
28 |
29 | body.theme-dark
30 | --highlight-color #ffbe42
31 | --secondary-color #82cdb1
32 |
33 | --body-bg #0b121d
34 | --body-fg #fff
35 |
36 | --header-fg #fff
37 | --header-bg #0d0d0f
38 |
39 | --input-bg #0b121d
40 | --input-border-color #666
41 |
42 | --panel-bg #0d0d0f
43 |
44 | --code-bg #888
45 | --code-fg #eee
46 |
47 | --border-color #223c5f
48 |
49 | --button-icon-fg #eee
50 | --button-disabled-fg #666
51 |
52 | --title-color #8cb5f1
53 |
54 | --log-fg #7f7f7f
55 | --log-bg #000
56 |
57 | --overlay-bg rgba(64, 64, 64, 0.6)
58 |
59 | --shadow-color #000
60 |
61 | --table-border-color #000
62 | --table-header-bg #2a3d4f
63 | --table-header-fg #f1f1f1
64 | --table-even #0f1319
65 | --table-odd #1a2835
66 |
67 | --border-radius 3px
68 | --border 1px solid var(--border-color)
69 |
70 | --disconnected-bg #333
71 |
72 | .machine-view .machine-group-header
73 | border-top var(--border)
74 |
--------------------------------------------------------------------------------
/src/data-series.js:
--------------------------------------------------------------------------------
1 | /******************************************************************************\
2 |
3 | This file is part of the Folding@home Client.
4 |
5 | The fah-client runs Folding@home protein folding simulations.
6 | Copyright (c) 2001-2024, foldingathome.org
7 | All rights reserved.
8 |
9 | This program is free software; you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation; either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License along
20 | with this program; if not, write to the Free Software Foundation, Inc.,
21 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 |
23 | For information regarding this software email:
24 | Joseph Coffland
25 | joseph@cauldrondevelopment.com
26 |
27 | \******************************************************************************/
28 |
29 | import {reactive} from 'vue'
30 |
31 |
32 | class DataSeries {
33 | constructor(color, enabled = true) {
34 | this.color = color
35 | this.enabled = enabled
36 | this.state = reactive({
37 | data: [],
38 | min: {x: Infinity, y: Infinity},
39 | max: {x: -Infinity, y: -Infinity},
40 | })
41 | }
42 |
43 |
44 | get data() {return this.state.data}
45 | get max() {return this.state.max}
46 | get min() {return this.state.min}
47 |
48 |
49 | add(data) {
50 | this.state.data.push(data)
51 |
52 | this.state.min.x = Math.min(this.state.min.x, data.x)
53 | this.state.max.x = Math.max(this.state.max.x, data.x)
54 | this.state.min.y = Math.min(this.state.min.y, data.y)
55 | this.state.max.y = Math.max(this.state.max.y, data.y)
56 | }
57 |
58 |
59 | find_nearest_x(x) {
60 | let data = this.state.data
61 | if (!data.length) return
62 |
63 | let i = this._find_index(data, x, 0, data.length)
64 |
65 | if (i == data.length) return data[i - 1]
66 | if (i == 0) return data[0]
67 | return data[x - data[i - 1].x < data[i].x - x ? i - 1 : i]
68 | }
69 |
70 |
71 | _find_index(data, x, min, max) {
72 | let len = max - min
73 |
74 | if (len == 1) return data[min].x < x ? min + 1 : min
75 |
76 | let mid = Math.floor(len / 2) + min
77 |
78 | if (x < data[mid].x) return this._find_index(data, x, min, mid)
79 | return this._find_index(data, x, mid, max)
80 | }
81 | }
82 |
83 |
84 | export default DataSeries
85 |
--------------------------------------------------------------------------------
/src/direct-mach-conn.js:
--------------------------------------------------------------------------------
1 | /******************************************************************************\
2 |
3 | This file is part of the Folding@home Client.
4 |
5 | The fah-client runs Folding@home protein folding simulations.
6 | Copyright (c) 2001-2024, foldingathome.org
7 | All rights reserved.
8 |
9 | This program is free software; you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation; either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License along
20 | with this program; if not, write to the Free Software Foundation, Inc.,
21 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 |
23 | For information regarding this software email:
24 | Joseph Coffland
25 | joseph@cauldrondevelopment.com
26 |
27 | \******************************************************************************/
28 |
29 | import MachConnection from './mach-connection.js'
30 | import Sock from './sock.js'
31 |
32 |
33 | class DirectMachConn extends MachConnection {
34 | constructor(ctx, name, address = '127.0.0.1:7396') {
35 | super(ctx.$machs.create(undefined))
36 |
37 | this.ctx = ctx
38 | this.initialized = false
39 |
40 | this.mach.set_conn(this)
41 | this.set_address(address)
42 | this.ctx.$machs.set('__direct__', this.mach)
43 | }
44 |
45 |
46 | set_address(address) {
47 | if (this.address == address) return
48 | this.address = address
49 |
50 | if (this.sock) {
51 | this.sock.on_open = () => {}
52 | this.sock.on_close = () => {}
53 | this.sock.on_message = () => {}
54 | this.sock.close()
55 | delete this.sock
56 | this._on_close()
57 | }
58 |
59 | this.mach.set_name('direct')
60 | this.mach.state.data = {}
61 |
62 | let url = 'ws://' + address + '/api/websocket'
63 | this.sock = new Sock(url)
64 | this.sock.on_open = () => this._on_open()
65 | this.sock.on_close = event => this._on_close(event)
66 | this.sock.on_message = msg => this._on_message(msg)
67 |
68 | this.open()
69 | }
70 |
71 |
72 | open() {this.sock.connect()}
73 |
74 |
75 | // From MachConnection
76 | is_connected() {return this.sock.connected}
77 | is_direct() {return true}
78 | async send(msg) {return this.sock.send(msg)}
79 |
80 |
81 | _clear_ping() {
82 | if (this._ping_timer != undefined) clearTimeout(this._ping_timer)
83 | delete this._ping_timer
84 | }
85 |
86 |
87 | _update_ping() {
88 | if (this.ctx.$util.version_less('8.1.17', this.mach.get_version())) {
89 | this._clear_ping()
90 | this._ping_timer = setTimeout(() => {
91 | console.log(this.mach.get_name() + ': timed out')
92 | this.sock.close()
93 | }, 30000)
94 | }
95 | }
96 |
97 |
98 | _on_open(event) {this.on_open()}
99 |
100 |
101 | _on_close(event) {
102 | this._clear_ping()
103 | this.on_close()
104 | this.initialized = false
105 | if (this.sock) setTimeout(() => this.sock.connect(), 1000)
106 | }
107 |
108 |
109 | _on_message(msg) {
110 | this._update_ping()
111 | this.on_message(msg)
112 |
113 | if (!this.initialized) {
114 | let info = this.mach.get_info()
115 |
116 | if (info.version) {
117 | this.initialized = true
118 |
119 | // Check versions, reload Web Control if out of date
120 | console.debug('Direct Client Version', info.version)
121 | let last_version = this.ctx.$util.retrieve('fah-last-version')
122 | let our_version = import.meta.env.PACKAGE_VERSION
123 |
124 | if (this.ctx.$util.version_less(our_version, info.version) &&
125 | (!last_version ||
126 | this.ctx.$util.version_less(last_version, info.version))) {
127 | this.ctx.$util.store('fah-last-version', info.version)
128 |
129 | if (location.hostname.indexOf('foldingathome.org') != -1) {
130 | if (!info.url) location.reload(true)
131 | else location.replace(info.url)
132 | }
133 | }
134 |
135 | // Set direct connection
136 | if (info.id) {
137 | let node_mach = this.ctx.$machs.get(info.id)
138 | if (node_mach) this.mach.dup_state(node_mach)
139 | this.mach.state.id = info.id
140 | }
141 |
142 | // Update machine name
143 | if (info.mach_name) this.mach.set_name(info.mach_name)
144 |
145 | this.mach.auto_link()
146 | }
147 | }
148 | }
149 | }
150 |
151 |
152 | export default DirectMachConn
153 |
--------------------------------------------------------------------------------
/src/light.styl:
--------------------------------------------------------------------------------
1 | /******************************************************************************\
2 |
3 | This file is part of the Folding@home Client.
4 |
5 | The fah-client runs Folding@home protein folding simulations.
6 | Copyright (c) 2001-2024, foldingathome.org
7 | All rights reserved.
8 |
9 | This program is free software; you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation; either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License along
20 | with this program; if not, write to the Free Software Foundation, Inc.,
21 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 |
23 | For information regarding this software email:
24 | Joseph Coffland
25 | joseph@cauldrondevelopment.com
26 |
27 | \******************************************************************************/
28 |
29 | body
30 | --highlight-color #444
31 | --secondary-color #888
32 |
33 | --error-color #b51100
34 | --success-color #4caf50
35 | --warn-color #fb0
36 |
37 | --body-bg #e7e7e7
38 | --body-fg #333
39 |
40 | --header-fg #333
41 | --header-bg #fff
42 |
43 | --input-bg #fff
44 | --input-border-color #eee
45 | --input-border 2px inset var(--input-border-color)
46 |
47 | --panel-bg #fff
48 | --panel-fg var(--body-fg)
49 |
50 | --link-color #ee9322
51 | --link-alt #55aaff
52 |
53 | --logo-color var(--header-fg)
54 |
55 | --log-fg var(--panel-fg)
56 | --log-bg var(--panel-bg)
57 |
58 | --title-color inherit
59 | --subtitle-color var(--secondary-color)
60 |
61 | --button-bg #0b5ed7
62 | --button-fg #fff
63 | --button-icon-fg #000
64 | --button-success var(--success-color)
65 | --button-caution var(--error-color)
66 | --button-disabled-bg #aaa
67 | --button-disabled-fg #ddd
68 |
69 | --pacify-bg rgba(0, 0, 0, 0.6)
70 |
71 | --overlay-bg rgba(0, 0, 0, 0.4)
72 | --overlay-fg #fff
73 |
74 | --code-bg #ddd
75 | --code-fg #444
76 |
77 | --border-color var(--panel-bg)
78 |
79 | --table-border-color #ddd
80 | --table-border 1px solid var(--table-border-color)
81 | --table-header-bg #dedede
82 | --table-header-fg #333
83 | --table-even #fff
84 | --table-odd #f3f3f3
85 |
86 | --shadow-color #222
87 | --shadow 3px 3px 12px var(--shadow-color)
88 |
89 | --border-radius 0
90 | --border 0
91 |
92 | --disconnected-bg #ddd
93 |
--------------------------------------------------------------------------------
/src/mach-connection.js:
--------------------------------------------------------------------------------
1 | /******************************************************************************\
2 |
3 | This file is part of the Folding@home Client.
4 |
5 | The fah-client runs Folding@home protein folding simulations.
6 | Copyright (c) 2001-2024, foldingathome.org
7 | All rights reserved.
8 |
9 | This program is free software; you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation; either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License along
20 | with this program; if not, write to the Free Software Foundation, Inc.,
21 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 |
23 | For information regarding this software email:
24 | Joseph Coffland
25 | joseph@cauldrondevelopment.com
26 |
27 | \******************************************************************************/
28 |
29 | class MachConnection {
30 | constructor(mach) {this.mach = mach}
31 |
32 | is_connected() {return false}
33 | is_direct() {return false}
34 |
35 | get_id() {return this.mach.get_id()}
36 |
37 | on_open() {this.mach.on_open()}
38 | on_close() {this.mach.on_close()}
39 | on_message(msg) {this.mach.on_message(msg)}
40 |
41 | async send(msg) {}
42 | async receive(msg) {}
43 | close() {}
44 | }
45 |
46 | export default MachConnection
47 |
--------------------------------------------------------------------------------
/src/machines.js:
--------------------------------------------------------------------------------
1 | /******************************************************************************\
2 |
3 | This file is part of the Folding@home Client.
4 |
5 | The fah-client runs Folding@home protein folding simulations.
6 | Copyright (c) 2001-2024, foldingathome.org
7 | All rights reserved.
8 |
9 | This program is free software; you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation; either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License along
20 | with this program; if not, write to the Free Software Foundation, Inc.,
21 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 |
23 | For information regarding this software email:
24 | Joseph Coffland
25 | joseph@cauldrondevelopment.com
26 |
27 | \******************************************************************************/
28 |
29 | import {watchEffect, reactive, toRaw} from 'vue'
30 | import Machine from './machine.js'
31 | import Unit from './unit.js'
32 |
33 |
34 | class Machines {
35 | constructor(ctx) {
36 | this.ctx = ctx
37 | this.machines = reactive({})
38 |
39 | watchEffect(() => {
40 | if (!ctx.$account.data.machines) return
41 |
42 | // Add new machines and set machine name
43 | let found = {}
44 | for (let config of ctx.$account.data.machines) {
45 | let mach = this.get(config.id)
46 | if (!mach) mach = this.add(config.id)
47 | mach.set_name(config.name)
48 | found[config.id] = true
49 | }
50 |
51 | // Erase removed machines
52 | for (let mach of this)
53 | if (!found[mach.get_id()]) {
54 | if (mach.is_direct()) continue // Don't remove direct connections
55 | mach.close()
56 | this.del(mach.get_id())
57 | }
58 | })
59 | }
60 |
61 |
62 | get is_empty() {return !this.count}
63 | get count() {return Array.from(this).length}
64 |
65 |
66 | *[Symbol.iterator]() {
67 | for (let mach of Object.values(this.machines))
68 | if (!mach.is_hidden()) yield mach
69 | }
70 |
71 |
72 | set(id, mach) {
73 | if (this.machines[id]) {
74 | if (toRaw(this.machines[id]) == mach) return
75 | this.machines[id].close()
76 | }
77 |
78 | this.machines[id] = mach
79 | mach.wus_enable(this.wus_enabled)
80 | }
81 |
82 |
83 | has(id) {return id in this.machines}
84 | get(id) {return this.machines[id]}
85 | add(id) {this.set(id, this.create(id)); return this.get(id)}
86 | del(id) {delete this.machines[id]}
87 | create(id) {return new Machine(id, this.ctx)}
88 |
89 |
90 | get_direct_id() {
91 | let mach = this.get_direct()
92 | if (mach) return mach.get_id()
93 | }
94 |
95 |
96 | get_direct() {return this.machines['__direct__']}
97 |
98 |
99 | get_direct_config(group) {
100 | let mach = this.get_direct()
101 | return mach ? mach.get_config(group) : {}
102 | }
103 |
104 |
105 | *get_units() {
106 | let found = {}
107 |
108 | for (let mach of this) {
109 | let units = (mach.get_data().wus || []).concat(mach.get_units())
110 |
111 | for (let unit of units) {
112 | if (!(unit instanceof Unit)) unit = new Unit(this.ctx, unit, mach)
113 |
114 | if (unit.id && unit.project && !found[unit.id]) {
115 | found[unit.id] = true
116 | yield unit
117 | }
118 | }
119 | }
120 | }
121 |
122 |
123 | get_unit(id) {
124 | for (let unit of this.get_units())
125 | if (unit.id == id) return unit
126 | return {}
127 | }
128 |
129 |
130 | active_unit_sum(fn) {
131 | return Array.from(this).reduce((sum, mach) => {
132 | if (!mach.is_recently_connected) return sum
133 |
134 | return mach.get_units().reduce((sum, unit) => {
135 | if (unit.state != 'RUN' && !unit.finish) return sum
136 | let value = fn(unit)
137 | return sum + (isFinite(value) ? value : 0)
138 | }, sum)
139 | }, 0)
140 | }
141 |
142 |
143 | get ppd() {return this.active_unit_sum(unit => unit.unit.ppd)}
144 |
145 |
146 | async set_state(state) {
147 | await this.ctx.$node.broadcast('state', {state})
148 |
149 | for (let mach of this)
150 | if (mach.is_direct())
151 | await mach.set_state(state)
152 | }
153 |
154 |
155 | wus_enable(enable) {
156 | for (let mach of this)
157 | mach.wus_enable(enable)
158 |
159 | this.wus_enabled = enable
160 | }
161 | }
162 |
163 | export default Machines
164 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | /******************************************************************************\
2 |
3 | This file is part of the Folding@home Client.
4 |
5 | The fah-client runs Folding@home protein folding simulations.
6 | Copyright (c) 2001-2024, foldingathome.org
7 | All rights reserved.
8 |
9 | This program is free software; you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation; either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License along
20 | with this program; if not, write to the Free Software Foundation, Inc.,
21 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 |
23 | For information regarding this software email:
24 | Joseph Coffland
25 | joseph@cauldrondevelopment.com
26 |
27 | \******************************************************************************/
28 |
29 | import {createApp} from 'vue'
30 | import App from './App.vue'
31 | import router from './router'
32 | import Button from './Button.vue'
33 | import Dialog from './Dialog.vue'
34 | import ProgressBar from './ProgressBar.vue'
35 | import DragList from './DragList.vue'
36 | import InfoItem from './InfoItem.vue'
37 | import Award from './Award.vue'
38 | import HelpBalloon from './HelpBalloon.vue'
39 | import ImageInput from './ImageInput.vue'
40 | import PlotView from './PlotView.vue'
41 | import FAHLogo from './FAHLogo.vue'
42 | import ClientVersion from './ClientVersion.vue'
43 | import ViewHeader from './ViewHeader.vue'
44 | import MainHeader from './MainHeader.vue'
45 | import ProjectView from './ProjectView.vue'
46 | import UnitHeader from './UnitHeader.vue'
47 | import UnitHeaders from './UnitHeaders.vue'
48 | import UnitsView from './UnitsView.vue'
49 | import UnitField from './UnitField.vue'
50 | import UnitInfo from './UnitInfo.vue'
51 | import Cache from './cache.js'
52 | import API from './api.js'
53 | import APISock from './api-sock.js'
54 | import Account from './account.js'
55 | import Util from './util.js'
56 | import Crypto from './crypto.js'
57 | import Node from './node.js'
58 | import Machines from './machines.js'
59 | import Stats from './stats.js'
60 | import Projects from './projects.js'
61 | import News from './news.js'
62 | import DirectMachConn from './direct-mach-conn.js'
63 |
64 |
65 | function add_components(app, components) {
66 | for (let [name, component] of Object.entries(components))
67 | app.component(name, component)
68 | }
69 |
70 |
71 | async function main(url) {
72 | const app = createApp(App);
73 | const ctx = app.config.globalProperties
74 | ctx.$ctx = ctx
75 | ctx.$util = new Util
76 | ctx.$crypto = new Crypto(ctx)
77 | ctx.$cache = new Cache('fah')
78 | ctx.$api = new API(ctx, url)
79 | ctx.$apiSock = new APISock(ctx, 'https://ws.foldingathome.org/')
80 | ctx.$account = new Account(ctx)
81 | ctx.$adata = await ctx.$account.try_login()
82 | ctx.$machs = new Machines(ctx)
83 | ctx.$node = new Node(ctx)
84 | ctx.$projects = new Projects(ctx)
85 | ctx.$stats = new Stats(ctx)
86 | ctx.$news = new News(ctx)
87 |
88 | let addr = ctx.$util.get_direct_address()
89 | ctx.$direct = new DirectMachConn(ctx, 'local', addr)
90 |
91 | console.debug({account: Object.assign({}, ctx.$adata)})
92 |
93 | app.use(router)
94 | add_components(app, {
95 | Button, Dialog, ProgressBar, Award, HelpBalloon, FAHLogo, ClientVersion,
96 | ViewHeader, MainHeader, ProjectView, InfoItem, DragList, UnitHeader,
97 | UnitsView, ImageInput, PlotView, UnitField, UnitHeaders, UnitInfo
98 | })
99 |
100 | app.mount('#app')
101 | }
102 |
103 |
104 | console.debug('Web Control Version', import.meta.env.PACKAGE_VERSION)
105 | main(import.meta.env.VITE_API_URL || 'https://api.foldingathome.org')
106 |
--------------------------------------------------------------------------------
/src/matrix.js:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | This file is part of the Folding@home Client.
3 |
4 | The fah-client runs Folding@home protein folding simulations.
5 | Copyright (c) 2001-2024, foldingathome.org
6 | All rights reserved.
7 |
8 | This program is free software; you can redistribute it and/or modify
9 | it under the terms of the GNU General Public License as published by
10 | the Free Software Foundation; either version 3 of the License, or
11 | (at your option) any later version.
12 |
13 | This program is distributed in the hope that it will be useful,
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | GNU General Public License for more details.
17 |
18 | You should have received a copy of the GNU General Public License along
19 | with this program; if not, write to the Free Software Foundation, Inc.,
20 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 |
22 | For information regarding this software email:
23 | Joseph Coffland
24 | joseph@cauldrondevelopment.com
25 | *******************************************************************************/
26 |
27 | class Matrix {
28 | constructor(m = [1, 0, 0, 0, 1, 0, 0, 0, 1]) {this.m = m}
29 |
30 |
31 | clone() {return new Matrix(this.m)}
32 |
33 |
34 | mul2d(v) {
35 | return {
36 | x: this.m[0] * v.x + this.m[1] * v.y + this.m[2],
37 | y: this.m[3] * v.x + this.m[4] * v.y + this.m[5],
38 | }
39 | }
40 |
41 |
42 | multiply(m) {
43 | const a = this.m
44 | const b = m instanceof Matrix ? m.m : m
45 |
46 | this.m = [
47 | a[0] * b[0] + a[1] * b[3] + a[2] * b[6],
48 | a[0] * b[1] + a[1] * b[4] + a[2] * b[7],
49 | a[0] * b[2] + a[1] * b[5] + a[2] * b[8],
50 | a[3] * b[0] + a[4] * b[3] + a[5] * b[6],
51 | a[3] * b[1] + a[4] * b[4] + a[5] * b[7],
52 | a[3] * b[2] + a[4] * b[5] + a[5] * b[8],
53 | a[6] * b[0] + a[7] * b[3] + a[8] * b[6],
54 | a[6] * b[1] + a[7] * b[4] + a[8] * b[7],
55 | a[6] * b[2] + a[7] * b[5] + a[8] * b[8],
56 | ]
57 | }
58 |
59 |
60 | rotate(a) {
61 | const c = cos(n), s = sin(n)
62 | this.multiply([c, -s, 0, s, c, 0, 0, 0, 1])
63 | }
64 |
65 |
66 | translate(x = 0, y = 0) {this.multiply([1, 0, x, 0, 1, y, 0, 0, 1])}
67 | scale (x = 1, y = 1) {this.multiply([x, 0, 0, 0, y, 0, 0, 0, 1])}
68 |
69 |
70 | inverse() {
71 | let m = this.m
72 |
73 | let b = [
74 | m[8] * m[4] - m[5] * m[7],
75 | -m[8] * m[1] + m[2] * m[7],
76 | m[5] * m[1] - m[2] * m[4],
77 | -m[8] * m[3] + m[5] * m[6],
78 | m[8] * m[0] - m[2] * m[6],
79 | -m[5] * m[0] + m[2] * m[3],
80 | m[7] * m[3] - m[4] * m[6],
81 | -m[7] * m[0] + m[1] * m[6],
82 | m[4] * m[0] - m[1] * m[3],
83 | ]
84 |
85 | let det = m[0] * b[0] + m[1] * b[3] + m[2] * b[6]
86 | if (!det) return
87 | det = 1.0 / det
88 |
89 | return new Matrix(b.map(x => x * det))
90 | }
91 | }
92 |
93 | export default Matrix
94 |
--------------------------------------------------------------------------------
/src/news.js:
--------------------------------------------------------------------------------
1 | /******************************************************************************\
2 |
3 | This file is part of the Folding@home Client.
4 |
5 | The fah-client runs Folding@home protein folding simulations.
6 | Copyright (c) 2001-2024, foldingathome.org
7 | All rights reserved.
8 |
9 | This program is free software; you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation; either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License along
20 | with this program; if not, write to the Free Software Foundation, Inc.,
21 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 |
23 | For information regarding this software email:
24 | Joseph Coffland
25 | joseph@cauldrondevelopment.com
26 |
27 | \******************************************************************************/
28 |
29 | import {reactive} from 'vue'
30 |
31 |
32 | class News {
33 | constructor(ctx, url = 'https://foldingathome.org/wp-json/wp/v2',
34 | timeout = 24 * 60 * 60 * 1000) {
35 | this.cache = ctx.$cache
36 | this.url = url
37 | this.timeout = timeout
38 | this.data = reactive({
39 | authors: {},
40 | feed: [],
41 | })
42 |
43 | this._update()
44 | }
45 |
46 |
47 | get_feed() {return this.data.feed}
48 |
49 |
50 | set_feed(feed) {
51 | feed = feed.slice() // Copy
52 | let result = []
53 |
54 | while (feed.length) {
55 | // Choose next article giving newer articles a higher probability
56 | let r = Math.random()
57 | let x = Math.floor(-Math.log(r) / Math.log(3 / 2))
58 | let i = (r == 0 || feed.length <= x) ? 0 : x
59 |
60 | result.push(feed[i])
61 | feed.splice(i, 1)
62 | }
63 |
64 | this.data.feed = result
65 | }
66 |
67 |
68 | async get_featured_image(article, post) {
69 | let url = `${this.url}/media/${post.featured_media}?context=embed`
70 | let r = await fetch(url)
71 | let media = await r.json()
72 | let details = media.media_details || {}
73 | article.image = ((details.sizes || {}).medium || {}).source_url
74 | }
75 |
76 |
77 | async get_author(article, post) {
78 | if (post.author in this.data.authors) {
79 | article.author = this.data.authors[post.author]
80 | return
81 | }
82 |
83 | let r = await fetch(`${this.url}/users/${post.author}`)
84 | let author = await r.json()
85 | this.data.authors[post.author] = author.name
86 | article.author = author.name
87 | }
88 |
89 |
90 | async _update() {
91 | try {
92 | await this._load_feed()
93 | } catch (e) {console.log(e)}
94 |
95 | setTimeout(() => this._update(), 60 * 60 * 1000)
96 | }
97 |
98 |
99 | async _load_feed() {
100 | // Check cache
101 | let data = await this.cache.get('news', this.timeout)
102 | if (data) return this.set_feed(data)
103 |
104 | // Download feed
105 | let r = await fetch(`${this.url}/posts?context=embed`)
106 | let posts = await r.json()
107 |
108 | let feed = []
109 | let promises = []
110 |
111 | for (const post of posts) {
112 | let desc = post.excerpt.rendered
113 | .replace('>Read more<', 'target="_blank">Read more<')
114 |
115 | let article = reactive({
116 | url: post.link,
117 | title: post.title.rendered,
118 | date: new Date(post.date).toDateString(),
119 | description: desc
120 | })
121 | feed.push(article)
122 |
123 | promises.push(this.get_featured_image(article, post))
124 | promises.push(this.get_author(article, post))
125 | }
126 |
127 | if (!feed.length) return
128 | this.set_feed(feed)
129 |
130 | // Cache results
131 | await Promise.all(promises)
132 | await this.cache.set('news', feed)
133 | }
134 | }
135 |
136 | export default News
137 |
--------------------------------------------------------------------------------
/src/node-mach-conn.js:
--------------------------------------------------------------------------------
1 | /******************************************************************************\
2 |
3 | This file is part of the Folding@home Client.
4 |
5 | The fah-client runs Folding@home protein folding simulations.
6 | Copyright (c) 2001-2024, foldingathome.org
7 | All rights reserved.
8 |
9 | This program is free software; you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation; either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License along
20 | with this program; if not, write to the Free Software Foundation, Inc.,
21 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 |
23 | For information regarding this software email:
24 | Joseph Coffland
25 | joseph@cauldrondevelopment.com
26 |
27 | \******************************************************************************/
28 |
29 | import MachConnection from './mach-connection.js'
30 |
31 |
32 | class NodeMachConn extends MachConnection {
33 | constructor(ctx, mach, key) {
34 | super(mach)
35 | this.ctx = ctx
36 | this.key = key
37 | this.ivs = {}
38 | }
39 |
40 |
41 | async open() {
42 | await this._send({
43 | type: 'session-open',
44 | session: this.ctx.$node.sid,
45 | })
46 |
47 | this.on_open()
48 | }
49 |
50 |
51 | close() {this.on_close()}
52 |
53 |
54 | // From MachConnection
55 | is_connected() {return true}
56 |
57 |
58 | async send(msg) {
59 | return this._send({
60 | type: 'message',
61 | session: this.ctx.$node.sid,
62 | content: msg,
63 | })
64 | }
65 |
66 |
67 | async _send(msg) {
68 | console.debug('Sending:', msg)
69 |
70 | let iv = this.ctx.$crypto.get_random(16)
71 |
72 | let payload = JSON.stringify(msg)
73 | payload = await this.ctx.$crypto.aes(this.key, iv, payload, true)
74 | payload = this.ctx.$util.urlbase64_encode(payload)
75 |
76 | iv = this.ctx.$util.urlbase64_encode(iv)
77 | this.ivs[iv] = true
78 |
79 | msg = {type: 'message', id: this.get_id(), iv, payload}
80 | return this.ctx.$node.send(msg)
81 | }
82 |
83 |
84 | async receive(msg) {
85 | // Check that this is a new IV. Also prevents replay attacks.
86 | let iv = msg.iv
87 | if (this.ivs[iv]) throw 'IV cannot be used more than once'
88 | if (1e6 < this.ivs.length) throw 'Too many IVs'
89 | this.ivs[iv] = true
90 | iv = this.ctx.$util.base64_decode(iv)
91 |
92 | let payload = this.ctx.$util.base64_decode(msg.payload)
93 | payload = await this.ctx.$crypto.aes(this.key, iv, payload, false)
94 |
95 | // Decompress
96 | if (msg.compression)
97 | payload = await this.ctx.$util.decompress(payload, msg.compression)
98 |
99 | payload = JSON.parse(payload)
100 |
101 | if (payload.session != this.ctx.$node.sid)
102 | throw 'Message not for this session'
103 |
104 | // TODO find correct machine instance for payload.group
105 |
106 | // Process message content
107 | this.on_message(payload.content)
108 | }
109 | }
110 |
111 |
112 | export default NodeMachConn
113 |
--------------------------------------------------------------------------------
/src/projects.js:
--------------------------------------------------------------------------------
1 | /******************************************************************************\
2 |
3 | This file is part of the Folding@home Client.
4 |
5 | The fah-client runs Folding@home protein folding simulations.
6 | Copyright (c) 2001-2024, foldingathome.org
7 | All rights reserved.
8 |
9 | This program is free software; you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation; either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License along
20 | with this program; if not, write to the Free Software Foundation, Inc.,
21 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 |
23 | For information regarding this software email:
24 | Joseph Coffland
25 | joseph@cauldrondevelopment.com
26 |
27 | \******************************************************************************/
28 |
29 | import {watchEffect, reactive} from 'vue'
30 |
31 |
32 | class Projects {
33 | constructor(ctx, timeout = 24 * 60 * 60 * 1000) {
34 | this.ctx = ctx
35 | this.timeout = timeout
36 | this.state = reactive({
37 | loading: true,
38 | in_progress: {},
39 | projects: {},
40 | })
41 |
42 | watchEffect(() => this._update_ids())
43 | setTimeout(() => this.state.loading = false, 8000)
44 | }
45 |
46 |
47 | is_loading() {return this.state.loading}
48 |
49 |
50 | get(id) {
51 | if (id) return this.state.projects[id]
52 | return Object.values(this.state.projects)
53 | }
54 |
55 |
56 | _update_ids() {
57 | let projects = {}
58 |
59 | for (let unit of this.ctx.$machs.get_units())
60 | if (unit.assign.project) projects[unit.project] = true
61 |
62 | projects = Object.keys(projects)
63 |
64 | if (projects != this.state.ids) {
65 | this.state.ids = projects
66 | this._trigger_update()
67 | }
68 | }
69 |
70 |
71 | _trigger_update() {
72 | if (this.update_timer == undefined)
73 | this.update_timer = setTimeout(() => this._update(), 1000)
74 | }
75 |
76 |
77 | async _update() {
78 | delete this.update_timer
79 |
80 | // Load project data
81 | for (let id of this.state.ids)
82 | try {await this._load(id)} catch(e) {}
83 |
84 | // Remove old projects
85 | for (let id of Object.keys(this.state.projects))
86 | if (!id in this.state.ids) delete this.state.projects[id]
87 | }
88 |
89 |
90 | async _load(id) {
91 | if (this.state.projects[id] || this.state.in_progress[id]) return
92 |
93 | this.state.in_progress[id] = true
94 | try {
95 | let url = this.ctx.$api.url + '/project/' + id
96 | let data = await this.ctx.$api.fetch({
97 | path: '/project/' + id, expire: 0,
98 | action: 'Downloading project description.',
99 | error_cb: () => false // Don't show error message
100 | })
101 |
102 | if (!data.error) {
103 | data.id = parseInt(id)
104 | this.state.projects[id] = data
105 | }
106 |
107 | } finally {this.state.in_progress[id] = false}
108 | }
109 | }
110 |
111 | export default Projects
112 |
--------------------------------------------------------------------------------
/src/router.js:
--------------------------------------------------------------------------------
1 | /******************************************************************************\
2 |
3 | This file is part of the Folding@home Client.
4 |
5 | The fah-client runs Folding@home protein folding simulations.
6 | Copyright (c) 2001-2024, foldingathome.org
7 | All rights reserved.
8 |
9 | This program is free software; you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation; either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License along
20 | with this program; if not, write to the Free Software Foundation, Inc.,
21 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 |
23 | For information regarding this software email:
24 | Joseph Coffland
25 | joseph@cauldrondevelopment.com
26 |
27 | \******************************************************************************/
28 |
29 | import {createRouter, createWebHistory} from 'vue-router'
30 | import StatsView from './StatsView.vue'
31 | import MachinesView from './MachinesView.vue'
32 | import NewsView from './NewsView.vue'
33 | import ProjectsView from './ProjectsView.vue'
34 | import WUsView from './WUsView.vue'
35 | import SettingsView from './SettingsView.vue'
36 | import Visualization from './Visualization.vue'
37 | import LogView from './LogView.vue'
38 | import MachineMux from './MachineMux.vue'
39 | import MachineDetailsView from './MachineDetailsView.vue'
40 | import UnitDetailsView from './UnitDetailsView.vue'
41 | import AccountView from './AccountView.vue'
42 | import VerifyView from './VerifyView.vue'
43 | import ResetView from './ResetView.vue'
44 | import AccountSettings from './AccountSettings.vue'
45 | import AccountAppearance from './AccountAppearance.vue'
46 | import AccountTeams from './AccountTeams.vue'
47 |
48 |
49 | export default createRouter({
50 | history: createWebHistory(),
51 | routes: [
52 | {path: '/', redirect: '/machines'},
53 | {path: '/stats', component: StatsView},
54 | {path: '/machines', component: MachinesView},
55 | {path: '/projects', component: ProjectsView},
56 | {path: '/wus', component: WUsView},
57 | {path: '/news', component: NewsView},
58 | {path: '/unit/:unitID', component: UnitDetailsView, props: true},
59 | {path: '/verify/:token', component: VerifyView, props: true},
60 | {
61 | path: '/account/:tab?',
62 | component: AccountView,
63 | props: route => route.params,
64 | children: [
65 | {path: 'settings', component: AccountSettings},
66 | {path: 'appearance', component: AccountAppearance},
67 | {path: 'teams', component: AccountTeams},
68 | {path: ':pathMatch(.*)', redirect: '/account/settings'},
69 | ]
70 | }, {
71 | path: '/reset/:token',
72 | component: ResetView,
73 | props: route => Object.assign({email: route.query.email}, route.params)
74 | }, {
75 | path: '/:machID?',
76 | props: true,
77 | component: MachineMux,
78 | children: [
79 | {path: '', redirect: '/'},
80 | {path: 'settings', component: SettingsView},
81 | {path: 'details', component: MachineDetailsView},
82 | {path: 'view/:unitID', component: Visualization, props: true},
83 | {
84 | path: 'log',
85 | component: LogView,
86 | props: route => ({query: route.query.q})
87 | },
88 | {path: ':pathMatch(.*)*', redirect: '/'},
89 | ]
90 | },
91 | ]
92 | })
93 |
--------------------------------------------------------------------------------
/src/sock.js:
--------------------------------------------------------------------------------
1 | /******************************************************************************\
2 |
3 | This file is part of the Folding@home Client.
4 |
5 | The fah-client runs Folding@home protein folding simulations.
6 | Copyright (c) 2001-2024, foldingathome.org
7 | All rights reserved.
8 |
9 | This program is free software; you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation; either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License along
20 | with this program; if not, write to the Free Software Foundation, Inc.,
21 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 |
23 | For information regarding this software email:
24 | Joseph Coffland
25 | joseph@cauldrondevelopment.com
26 |
27 | \******************************************************************************/
28 |
29 | class Sock {
30 | constructor(url, timeout = 20000) {
31 | this.url = url
32 | this.timeout = timeout
33 | this.connected = false
34 | }
35 |
36 |
37 | set_url(url) {this.url = url}
38 | set_timeout(timeout) {this.timeout = timeout}
39 |
40 |
41 | on_message(msg) {console.log('WS:', msg)}
42 | on_open(event) {}
43 | on_close(event) {}
44 | on_error(event) {}
45 |
46 |
47 | _clear_timeout() {clearTimeout(this.timer)}
48 |
49 |
50 | _open(event) {
51 | this.connected = true
52 | this._clear_timeout()
53 | this.on_open(event)
54 | }
55 |
56 |
57 | _close(event) {
58 | this._clear_timeout()
59 | this.connected = false
60 | this.ws = undefined
61 | this.on_close(event)
62 | }
63 |
64 |
65 | _error(event) {this.on_error(event)}
66 | _message(event) {this.on_message(JSON.parse(event.data))}
67 | _timeout() {this.close()}
68 |
69 |
70 | close() {
71 | if (this.ws) this.ws.close()
72 | this._clear_timeout()
73 | }
74 |
75 |
76 | connect() {
77 | if (this.ws != undefined) return
78 |
79 | console.debug('Connecting to ' + this.url)
80 |
81 | this.ws = new WebSocket(this.url)
82 |
83 | this.ws.onopen = e => this._open(e)
84 | this.ws.onclose = e => this._close(e)
85 | this.ws.onerror = e => this._error(e)
86 | this.ws.onmessage = e => this._message(e)
87 |
88 | this.timer = setTimeout(() => this._timeout(), this.timeout)
89 | }
90 |
91 |
92 | send(msg) {
93 | if (this.connected) this.ws.send(JSON.stringify(msg))
94 | else console.debug('Cannot send message, not connected:', msg)
95 | }
96 | }
97 |
98 |
99 | export default Sock
100 |
--------------------------------------------------------------------------------
/src/stats.js:
--------------------------------------------------------------------------------
1 | /******************************************************************************\
2 |
3 | This file is part of the Folding@home Client.
4 |
5 | The fah-client runs Folding@home protein folding simulations.
6 | Copyright (c) 2001-2024, foldingathome.org
7 | All rights reserved.
8 |
9 | This program is free software; you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation; either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License along
20 | with this program; if not, write to the Free Software Foundation, Inc.,
21 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 |
23 | For information regarding this software email:
24 | Joseph Coffland
25 | joseph@cauldrondevelopment.com
26 |
27 | \******************************************************************************/
28 |
29 | import {watch, watchEffect, reactive} from 'vue'
30 |
31 |
32 | class Stats {
33 | constructor(ctx, timeout = 60 * 60 * 1000) {
34 | this.ctx = ctx
35 | this.api = ctx.$api
36 | this.adata = ctx.$account.data
37 | this.machs = ctx.$machs
38 | this.state = reactive({
39 | user: undefined,
40 | team: undefined,
41 | passkey: undefined,
42 | stats: {}
43 | })
44 | this.timeout = timeout
45 | this.url = 'https://stats.foldingathome.org'
46 |
47 | watch([
48 | () => this.state.user,
49 | () => this.state.team,
50 | () => this.state.passkey
51 | ], () => this._get_stats())
52 |
53 | watchEffect(() => this._update_config())
54 |
55 | this._update()
56 | }
57 |
58 |
59 | get_data() {return this.state.stats}
60 |
61 |
62 | get_team() {
63 | let ateam
64 | for (let team of (this.adata.teams || []))
65 | if (team.team == this.state.team) ateam = team
66 |
67 | for (let team of this.state.stats.teams || [])
68 | if (team.team == this.state.team) {
69 | if (ateam != undefined) return Object.assign({}, team, ateam)
70 | return team
71 | }
72 |
73 | return {}
74 | }
75 |
76 |
77 | get charts() {
78 | let charts = []
79 |
80 | let team = this.state.team
81 | if (team) charts.push({type: 'team', team})
82 |
83 | let uid = this.state.stats.id
84 | let pid = this.state.stats.pid || 0
85 | if (uid) charts.push({type: 'user', uid, pid, user: this.state.user})
86 |
87 | return charts
88 | }
89 |
90 |
91 | is_anon() {
92 | let user = this.state.user
93 | return !user || user.toLowerCase() == 'anonymous'
94 | }
95 |
96 |
97 | _update() {
98 | // Update stats periodically (cached up to `timeout`)
99 | setTimeout(() => this._update(), 60 * 1000)
100 | this._get_stats()
101 | }
102 |
103 |
104 | _get_config() {
105 | // Use account settings
106 | if (this.ctx.$account.logged_in) return this.adata
107 |
108 | // Otherwise use direct machine settings
109 | return this.machs.get_direct_config()
110 | }
111 |
112 |
113 | _update_config() {
114 | let {user, team, passkey} = this._get_config()
115 |
116 | this.state.user = user
117 | this.state.team = team
118 | this.state.passkey = passkey
119 | }
120 |
121 |
122 | async _get_team_stats(team) {
123 | let data = await this.api.fetch({
124 | path: `/team/${team}`, error_cb: () => false, expire: this.timeout})
125 |
126 | if (data && data.id != undefined)
127 | return Object.assign(data, {
128 | team,
129 | tscore: data.score,
130 | twus: data.wus,
131 | score: 0,
132 | wus: 0,
133 | })
134 |
135 | return {team, name: team, tscore: 0, twus: 0, score: 0, wus: 0}
136 | }
137 |
138 |
139 | async _get_stats() {
140 | let {user, team, passkey} = this.state
141 |
142 | if (this.is_anon() && !team) return this.state.stats = {}
143 |
144 | let path = `/user/${encodeURIComponent(user)}`
145 | let data = team == undefined ? {} : {team}
146 | if (this.state.passkey) data.passkey = this.state.passkey
147 |
148 | let stats = await this.api.fetch({
149 | path, data, error_cb: () => false, expire: this.timeout})
150 | if (stats && stats.name) this.state.stats = stats
151 |
152 | if (!this.state.stats) {
153 | this.state.stats = {
154 | name: user, id: 0, score: 0, wus: 0, active_7: 0, active_50: 0,
155 | teams: [await this._get_team_stats(team)]
156 | }
157 | }
158 | }
159 | }
160 |
161 |
162 | export default Stats
163 |
--------------------------------------------------------------------------------
/src/subscriber.js:
--------------------------------------------------------------------------------
1 | /******************************************************************************\
2 |
3 | This file is part of the Folding@home Client.
4 |
5 | The fah-client runs Folding@home protein folding simulations.
6 | Copyright (c) 2001-2025, foldingathome.org
7 | All rights reserved.
8 |
9 | This program is free software; you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation; either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License along
20 | with this program; if not, write to the Free Software Foundation, Inc.,
21 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 |
23 | For information regarding this software 358,797,681email:
24 | Joseph Coffland
25 | joseph@cauldrondevelopment.com
26 |
27 | \******************************************************************************/
28 |
29 | class Subscriber {
30 | constructor(sock, max_count = 10000, period = 3600) {
31 | this.sock = sock
32 | this.subscribers = {}
33 | this.data = []
34 | this.max_count = max_count
35 | this.period = period
36 | this.msg = {}
37 | }
38 |
39 |
40 | get has_subscribers() {return 0 < Object.keys(this.subscribers).length}
41 |
42 |
43 | add_subscriber(cb) {
44 | let id = this.sock.nextID++
45 | this.subscribers[id] = cb
46 | this.data.map(cb)
47 | this.update()
48 | return id
49 | }
50 |
51 |
52 | del_subscriber(id) {
53 | delete this.subscribers[id]
54 | this.update()
55 | }
56 |
57 |
58 | _get_message(type) {return Object.assign({type}, this.msg)}
59 |
60 |
61 | _cache_get_key(entry) {return '/?ts=' + new Date(entry.time).getTime()}
62 |
63 |
64 | async _cache_add(entry) {
65 | let res = new Response(JSON.stringify(entry))
66 | return this.cache.put(this._cache_get_key(entry), res)
67 | }
68 |
69 |
70 | async _cache_del(entry) {
71 | return this.cache.delete(this._cache_get_key(entry))
72 | }
73 |
74 |
75 | async _cache_load() {
76 | if (this.cache) return
77 | this.cache = await caches.open('fah-' + this.ref)
78 |
79 | let data = []
80 | let responses = await this.cache.matchAll('/', {ignoreSearch: true})
81 | for (let res of responses) {
82 | let entry = await res.json()
83 | let ts = new Date(entry.time).getTime()
84 | data.push([ts, entry])
85 | }
86 |
87 | // Sort the data descending in time
88 | data.sort((a, b) => b[0] < a[0])
89 | this.data = data.map(e => e[1])
90 | this._limit_data()
91 | this._notify(this.data)
92 | }
93 |
94 |
95 | async _subscribe() {
96 | await this._cache_load()
97 | if (!this.sock.connected) return this.sock.connect()
98 |
99 | let msg = this._get_message('subscribe')
100 | msg.max_count = this.max_count
101 | if (this.data.length) msg.since = this.data[this.data.length - 1].time
102 |
103 | this.sock.send(msg)
104 | this.subscribed = true
105 | }
106 |
107 |
108 | _unsubscribe() {
109 | if (this.sock.connected) this.sock.send(this._get_message('unsubscribe'))
110 | this.subscribed = false
111 | }
112 |
113 |
114 | _notify(data) {Object.values(this.subscribers).map(cb => {data.map(cb)})}
115 |
116 |
117 | update() {
118 | if (!this.subscribed && this.has_subscribers) this._subscribe()
119 | if (this.subscribed && !this.has_subscribers) this._unsubscribe()
120 | }
121 |
122 |
123 | _limit_data() {
124 | if (this.max_count < this.data.length) {
125 | let removed = this.data.splice(0, this.data.length - this.max_count)
126 | removed.map(entry => this._cache_del(entry))
127 | }
128 | }
129 |
130 |
131 | add_data(_data) {
132 | // Fill in missing data
133 | let data = []
134 | let last = this.data[this.data.length - 1]
135 |
136 | for (let entry of _data) {
137 | if (last != undefined) {
138 | let lastTime = new Date( last.time).getTime() / 1000
139 | let thisTime = new Date(entry.time).getTime() / 1000
140 | let steps = Math.round((thisTime - lastTime) / this.period)
141 |
142 | if (steps < this.max_count)
143 | for (let i = 1; i < steps; i++) {
144 | let time = new Date((lastTime + this.period * i) * 1000)
145 | data.push({time: time.toISOString(), value: last.value})
146 | }
147 |
148 | else {
149 | data = []
150 | this.data = []
151 | }
152 | }
153 |
154 | data.push(entry)
155 | last = entry
156 | }
157 |
158 | this._notify(data)
159 | this.data.push(...data)
160 | data.map(entry => this._cache_add(entry))
161 | this._limit_data()
162 | }
163 |
164 |
165 | on_message(msg) {
166 | let data = Array.isArray(msg.data) ? msg.data.reverse() : [msg.data]
167 | this.add_data(data)
168 | }
169 |
170 |
171 | on_open() {this.update()}
172 | on_close() {this.subscribed = false}
173 | }
174 |
175 |
176 | export default Subscriber
177 |
--------------------------------------------------------------------------------
/src/unit_fields.json:
--------------------------------------------------------------------------------
1 | {
2 | "Project": {"enabled": true, "minimal": true, "align": "left",
3 | "desc": "Project ID"},
4 | "Type": {"enabled": true, "desc": "CPU or GPU"},
5 | "CPUs": {"desc": "Number of CPUs in use"},
6 | "GPUs": {"desc": "Number of GPUs in use"},
7 | "GPUs Text": {"desc": "A description of the GPUs in use",
8 | "header": "GPUs"},
9 | "Status": {"enabled": true, "minimal": true, "align": "center",
10 | "desc": "Color coded status icon"},
11 | "Status Text": {"header": "Status", "align": "left",
12 | "desc": "Color coded status icon and text"},
13 | "Progress": {"enabled": true, "minimal": true, "size": "minmax(6em, 20em)",
14 | "align": "left", "desc": "Percent complete progress bar"},
15 | "PPD": {"enabled": true, "desc": "Estimated Points Per Day"},
16 | "ETA": {"desc": "Estimated time to Work Unit completion"},
17 | "TPF": {"desc": "Time Per Frame"},
18 | "OS": {"desc": "Operating System icon"},
19 | "OS Text": {"desc": "Operating System icon and name", "header": "OS"},
20 | "Core": {"desc": "Folding core hexadecimal ID"},
21 | "Deadline": {"desc": "Time until return deadline"},
22 | "Timeout": {"desc": "Time until bonus timeout"},
23 | "RCG": {"desc": "Run Clone Generation"},
24 | "Run Time": {"desc": "Total Work Unit run time"},
25 | "Base Credit": {"desc": "Work Unit base credit"},
26 | "Assign Time": {"desc": "Work Unit assignment time in UTC"},
27 | "Number": {"desc": "Work Unit number assigned by the machine it's on"},
28 | "Machine": {"desc": "The name of the machine", "align": "left"},
29 | "Work Server": {"desc": "Work Server that assigned the Work Unit"},
30 | "Version": {"desc": "Client version"},
31 | "Resources": {"desc": "CPU and GPU compute resources", "align": "left"},
32 | "Group Name": {"desc": "Resource group name", "header": "Group",
33 | "align": "left"}
34 | }
--------------------------------------------------------------------------------
/src/updatable.js:
--------------------------------------------------------------------------------
1 | /******************************************************************************\
2 |
3 | This file is part of the Folding@home Client.
4 |
5 | The fah-client runs Folding@home protein folding simulations.
6 | Copyright (c) 2001-2024, foldingathome.org
7 | All rights reserved.
8 |
9 | This program is free software; you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation; either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License along
20 | with this program; if not, write to the Free Software Foundation, Inc.,
21 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 |
23 | For information regarding this software email:
24 | Joseph Coffland
25 | joseph@cauldrondevelopment.com
26 |
27 | \******************************************************************************/
28 |
29 |
30 | function is_object(o) {return o != null && typeof o === 'object'}
31 |
32 |
33 | class Updatable {
34 | constructor(data) {Object.assign(this, Updatable.clean_keys(data))}
35 |
36 |
37 | static clean_key(key) {
38 | if (typeof key == 'string' && key.length <= 16)
39 | return key.replace('-', '_')
40 | return key
41 | }
42 |
43 |
44 | static clean_keys(data) {
45 | if (Array.isArray(data)) {
46 | let r = []
47 |
48 | for (const value of data)
49 | r.push(Updatable.clean_keys(value))
50 |
51 | return r
52 | }
53 |
54 | if (is_object(data)) {
55 | let r = {}
56 |
57 | for (const [key, value] of Object.entries(data))
58 | r[Updatable.clean_key(key)] = Updatable.clean_keys(value)
59 |
60 | return r
61 | }
62 |
63 | return data
64 | }
65 |
66 |
67 | do_update(update) {
68 | let obj = this
69 | let i = 0
70 |
71 | while (i < update.length - 2) {
72 | let key = Updatable.clean_key(update[i++])
73 |
74 | if (obj[key] == undefined)
75 | obj[key] = Number.isInteger(update[i]) ? [] : {}
76 |
77 | obj = obj[key]
78 | }
79 |
80 | let is_array = Array.isArray(obj)
81 | let key = Updatable.clean_key(update[i++])
82 | let value = update[i]
83 |
84 | if (is_array && key === -1) obj.push(value)
85 | else if (is_array && key === -2) obj.splice(obj.length, 0, ...value)
86 | else if (is_array && value === null) obj.splice(key, 1)
87 | else if (value === null) delete obj[key]
88 | else obj[key] = value
89 | }
90 | }
91 |
92 | export default Updatable
93 |
--------------------------------------------------------------------------------
/src/viewer/InfiniteGridHelper.js:
--------------------------------------------------------------------------------
1 | /******************************************************************************\
2 |
3 | This file is part of the Folding@home Client.
4 |
5 | The fah-client runs Folding@home protein folding simulations.
6 | Copyright (c) 2001-2024, foldingathome.org
7 | All rights reserved.
8 |
9 | This program is free software; you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation; either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License along
20 | with this program; if not, write to the Free Software Foundation, Inc.,
21 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 |
23 | For information regarding this software email:
24 | Joseph Coffland
25 | joseph@cauldrondevelopment.com
26 |
27 | \******************************************************************************/
28 |
29 | // Author: Fyrestar https://mevedia.com
30 | // (https://github.com/Fyrestar/THREE.InfiniteGridHelper)
31 | import * as THREE from 'three'
32 | import vertexShader from './grid.vert?raw'
33 | import fragmentShader from './grid.frag?raw'
34 |
35 |
36 | class InfiniteGridHelper extends THREE.Mesh {
37 | constructor(size1 = 10, size2 = 100, color = new THREE.Color('#888'),
38 | distance = 8000) {
39 | const matrix =
40 | new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(0, 1, 0), 1)
41 | matrix.premultiply(new THREE.Matrix4().makeTranslation(0, -100, 0))
42 |
43 | const geometry = new THREE.PlaneGeometry(2, 2, 1, 1)
44 | const material = new THREE.ShaderMaterial({
45 | side: THREE.DoubleSide,
46 | transparent: true,
47 | vertexShader,
48 | fragmentShader,
49 | extensions: {derivatives: true},
50 | uniforms: {
51 | uMatrix: new THREE.Uniform(matrix),
52 | uSize1: {value: size1},
53 | uSize2: {value: size2},
54 | uColor: {value: color},
55 | uDistance: {value: distance}
56 | }
57 | })
58 |
59 | super(geometry, material)
60 | this.frustumCulled = false
61 | }
62 | }
63 |
64 | export default InfiniteGridHelper
65 |
--------------------------------------------------------------------------------
/src/viewer/Sky.js:
--------------------------------------------------------------------------------
1 | /******************************************************************************\
2 |
3 | This file is part of the Folding@home Client.
4 |
5 | The fah-client runs Folding@home protein folding simulations.
6 | Copyright (c) 2001-2024, foldingathome.org
7 | All rights reserved.
8 |
9 | This program is free software; you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation; either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License along
20 | with this program; if not, write to the Free Software Foundation, Inc.,
21 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 |
23 | For information regarding this software email:
24 | Joseph Coffland
25 | joseph@cauldrondevelopment.com
26 |
27 | \******************************************************************************/
28 |
29 | /**
30 | * @author zz85 / https://github.com/zz85
31 | *
32 | * Based on "A Practical Analytic Model for Daylight"
33 | * aka The Preetham Model, the de facto standard analytic skydome model
34 | * http://www.cs.utah.edu/~shirley/papers/sunsky/sunsky.pdf
35 | *
36 | * First implemented by Simon Wallner
37 | * http://www.simonwallner.at/projects/atmospheric-scattering
38 | *
39 | * Improved by Martin Upitis
40 | * http://blenderartists.org/forum/showthread.php?
41 | * 245954-preethams-sky-implementation-HDR
42 | *
43 | * Three.js integration by zz85 http://twitter.com/blurspline
44 | *
45 | * Node.js module implementation by Danila Loginov https://loginov.rocks
46 | */
47 | import * as THREE from 'three'
48 | import vertexShader from './sky.vert?raw'
49 | import fragmentShader from './sky.frag?raw'
50 |
51 |
52 | class Sky extends THREE.Mesh {
53 | constructor() {
54 | const material = new THREE.ShaderMaterial({
55 | side: THREE.BackSide,
56 | fragmentShader,
57 | vertexShader,
58 | uniforms: {
59 | luminance: {value: 1},
60 | turbidity: {value: 10},
61 | rayleigh: {value: 2},
62 | mieCoefficient: {value: 0.005},
63 | mieDirectionalG: {value: 0.8},
64 | sunPosition: {value: new THREE.Vector3(-200, -3, -200)}
65 | }
66 | })
67 |
68 | super(new THREE.SphereGeometry(5000, 320, 150), material)
69 | }
70 | }
71 |
72 |
73 | export default Sky
74 |
--------------------------------------------------------------------------------
/src/viewer/grid.frag:
--------------------------------------------------------------------------------
1 | varying vec3 worldPosition;
2 | uniform float uSize1;
3 | uniform float uSize2;
4 | uniform vec3 uColor;
5 | uniform float uDistance;
6 | uniform mat4 uMatrix;
7 |
8 |
9 | float getGrid(float size) {
10 | vec2 r = worldPosition.xz / size;
11 | vec2 grid = abs(fract(r - 0.5) - 0.5) / fwidth(r);
12 | float line = min(grid.x, grid.y);
13 |
14 | return 1.0 - min(line, 1.0);
15 | }
16 |
17 |
18 | void main() {
19 | float d = 1.0 - min(distance(cameraPosition.xz,
20 | worldPosition.xz) / uDistance, 1.0);
21 | float g1 = getGrid(uSize1);
22 | float g2 = getGrid(uSize2);
23 |
24 | gl_FragColor = vec4(uColor.rgb, mix(g2, g1, g1) * pow(d, 3.0));
25 | gl_FragColor.a = mix(0.5 * gl_FragColor.a, gl_FragColor.a, g2);
26 |
27 | if (gl_FragColor.a <= 0.0) discard;
28 | }
29 |
--------------------------------------------------------------------------------
/src/viewer/grid.vert:
--------------------------------------------------------------------------------
1 | varying vec3 worldPosition;
2 | uniform float uDistance;
3 | uniform mat4 uMatrix;
4 |
5 |
6 | void main() {
7 | vec3 pos = position.xzy * uDistance;
8 | pos.xz += cameraPosition.xz;
9 | worldPosition = pos;
10 | gl_Position = projectionMatrix * modelViewMatrix * uMatrix * vec4(pos, 1.0);
11 | }
12 |
--------------------------------------------------------------------------------
/src/viewer/sky.frag:
--------------------------------------------------------------------------------
1 | varying vec3 vWorldPosition;
2 | varying vec3 vSunDirection;
3 | varying float vSunfade;
4 | varying vec3 vBetaR;
5 | varying vec3 vBetaM;
6 | varying float vSunE;
7 |
8 | uniform float luminance;
9 | uniform float mieDirectionalG;
10 |
11 | const vec3 cameraPos = vec3(0.0, 0.0, 0.0);
12 |
13 | // constants for atmospheric scattering
14 | const float pi = 3.141592653589793238462643383279502884197169;
15 |
16 | const float n = 1.0003; // refractive index of air
17 | const float N = 2.545E25; // number of molecules per unit volume for air at
18 | // 288.15K and 1013mb (sea level -45 celsius)
19 |
20 | // optical length at zenith for molecules
21 | const float rayleighZenithLength = 8.4E3;
22 | const float mieZenithLength = 1.25E3;
23 | const vec3 up = vec3(0.0, 1.0, 0.0);
24 |
25 | // 66 arc seconds -> degrees, and the cosine of that
26 | const float sunAngularDiameterCos =
27 | 0.999956676946448443553574619906976478926848692873900859324;
28 |
29 | const float THREE_OVER_SIXTEENPI = 0.05968310365946075; // 3.0 / (16.0 * pi)
30 | const float ONE_OVER_FOURPI = 0.07957747154594767; // 1.0 / (4.0 * pi)
31 |
32 |
33 | float rayleighPhase(float cosTheta) {
34 | return THREE_OVER_SIXTEENPI * (1.0 + pow(cosTheta, 2.0));
35 | }
36 |
37 |
38 | float hgPhase(float cosTheta, float g) {
39 | float g2 = pow(g, 2.0);
40 | float inverse = 1.0 / pow(1.0 - 2.0 * g * cosTheta + g2, 1.5);
41 | return ONE_OVER_FOURPI * ((1.0 - g2) * inverse);
42 | }
43 |
44 |
45 | // Filmic ToneMapping http://filmicgames.com/archives/75
46 | const float A = 0.15;
47 | const float B = 0.50;
48 | const float C = 0.10;
49 | const float D = 0.20;
50 | const float E = 0.02;
51 | const float F = 0.30;
52 |
53 | const float whiteScale = 1.0748724675633854; // 1.0 / Uncharted2Tonemap(1000.0)
54 |
55 |
56 | vec3 Uncharted2Tonemap(vec3 x) {
57 | return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F;
58 | }
59 |
60 |
61 | void main() {
62 | // optical length
63 | // cutoff angle at 90 to avoid singularity in next formula.
64 | float zenithAngle =
65 | acos(max(0.0, dot(up, normalize(vWorldPosition - cameraPos))));
66 | float inverse =
67 | 1.0 / (cos(zenithAngle) + 0.15 *
68 | pow(93.885 - ((zenithAngle * 180.0) / pi), -1.253));
69 | float sR = rayleighZenithLength * inverse;
70 | float sM = mieZenithLength * inverse;
71 |
72 | // combined extinction factor
73 | vec3 Fex = exp(-(vBetaR * sR + vBetaM * sM));
74 |
75 | // in scattering
76 | float cosTheta = dot(normalize(vWorldPosition - cameraPos), vSunDirection);
77 |
78 | float rPhase = rayleighPhase(cosTheta * 0.5 + 0.5);
79 | vec3 betaRTheta = vBetaR * rPhase;
80 |
81 | float mPhase = hgPhase(cosTheta, mieDirectionalG);
82 | vec3 betaMTheta = vBetaM * mPhase;
83 |
84 | vec3 Lin = pow(vSunE * ((betaRTheta + betaMTheta) /
85 | (vBetaR + vBetaM)) * (1.0 - Fex), vec3(1.5));
86 | Lin *= mix(vec3(1.0),
87 | pow(vSunE * ((betaRTheta + betaMTheta) / (vBetaR + vBetaM)) * Fex,
88 | vec3(1.0 / 2.0)),
89 | clamp(pow(1.0 - dot(up, vSunDirection), 5.0), 0.0, 1.0));
90 |
91 | // nightsky
92 | vec3 direction = normalize(vWorldPosition - cameraPos);
93 | float theta = acos(direction.y); // elevation --> y-axis, [-pi/2, pi/2]
94 | float phi =
95 | atan(direction.z, direction.x); // azimuth --> x-axis [-pi/2, pi/2]
96 | vec2 uv = vec2(phi, theta) / vec2(2.0 * pi, pi) + vec2(0.5, 0.0);
97 | vec3 L0 = vec3(0.1) * Fex;
98 |
99 | // composition + solar disc
100 | float sundisk = smoothstep(sunAngularDiameterCos,
101 | sunAngularDiameterCos + 0.00002, cosTheta);
102 | L0 += (vSunE * 19000.0 * Fex) * sundisk;
103 |
104 | vec3 texColor = (Lin + L0) * 0.04 + vec3(0.0, 0.0003, 0.00075);
105 | vec3 curr = Uncharted2Tonemap((log2(2.0 / pow(luminance, 4.0))) * texColor);
106 | vec3 color = curr * whiteScale;
107 | vec3 retColor = pow(color, vec3(1.0 / (1.2 + (1.2 * vSunfade))));
108 |
109 | gl_FragColor = vec4(retColor, 1.0);
110 | }
111 |
--------------------------------------------------------------------------------
/src/viewer/sky.vert:
--------------------------------------------------------------------------------
1 | uniform vec3 sunPosition;
2 | uniform float rayleigh;
3 | uniform float turbidity;
4 | uniform float mieCoefficient;
5 |
6 | varying vec3 vWorldPosition;
7 | varying vec3 vSunDirection;
8 | varying float vSunfade;
9 | varying vec3 vBetaR;
10 | varying vec3 vBetaM;
11 | varying float vSunE;
12 |
13 | const vec3 up = vec3(0.0, 1.0, 0.0);
14 |
15 | // constants for atmospheric scattering
16 | const float e = 2.71828182845904523536028747135266249775724709369995957;
17 | const float pi = 3.141592653589793238462643383279502884197169;
18 |
19 | // wavelength of used primaries, according to preetham
20 | const vec3 lambda = vec3(680E-9, 550E-9, 450E-9);
21 |
22 | // this pre-calculation replaces older TotalRayleigh(vec3 lambda) function:
23 | // (8.0 * pow(pi, 3.0) * pow(pow(n, 2.0) - 1.0, 2.0) *
24 | // (6.0 + 3.0 * pn)) / (3.0 * N * pow(lambda, vec3(4.0)) * (6.0 - 7.0 * pn))
25 | const vec3 totalRayleigh =
26 | vec3(5.804542996261093E-6, 1.3562911419845635E-5, 3.0265902468824876E-5);
27 |
28 | // mie stuff
29 | // K coefficient for the primaries
30 | const float v = 4.0;
31 | const vec3 K = vec3(0.686, 0.678, 0.666);
32 |
33 | // MieConst = pi * pow((2.0 * pi) / lambda, vec3(v - 2.0)) * K
34 | const vec3 MieConst =
35 | vec3(1.8399918514433978E14, 2.7798023919660528E14, 4.0790479543861094E14);
36 |
37 | // earth shadow hack
38 | // cutoffAngle = pi / 1.95
39 | const float cutoffAngle = 1.6110731556870734;
40 | const float steepness = 1.5;
41 | const float EE = 1000.0;
42 |
43 |
44 | float sunIntensity(float zenithAngleCos) {
45 | zenithAngleCos = clamp(zenithAngleCos, -1.0, 1.0);
46 | return EE * max(0.0, 1.0 -
47 | pow(e, -((cutoffAngle - acos(zenithAngleCos)) / steepness)));
48 | }
49 |
50 |
51 | vec3 totalMie(float T) {
52 | float c = (0.2 * T) * 10E-18;
53 | return 0.434 * c * MieConst;
54 | }
55 |
56 |
57 | void main() {
58 | vec4 worldPosition = modelMatrix * vec4(position, 1.0);
59 | vWorldPosition = worldPosition.xyz;
60 |
61 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
62 | vSunDirection = normalize(sunPosition);
63 | vSunE = sunIntensity(dot(vSunDirection, up));
64 | vSunfade = 1.0 - clamp(1.0 - exp((sunPosition.y / 450000.0)), 0.0, 1.0);
65 |
66 | float rayleighCoefficient = rayleigh - (1.0 * (1.0 - vSunfade));
67 |
68 | // extinction (absorption + out scattering) rayleigh coefficients
69 | vBetaR = totalRayleigh * rayleighCoefficient;
70 |
71 | // mie coefficients
72 | vBetaM = totalMie(turbidity) * mieCoefficient;
73 | }
74 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import {defineConfig} from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 | import packageJson from './package.json'
4 |
5 |
6 | export default defineConfig({
7 | preview: {port: 5173},
8 |
9 | plugins: [
10 | vue({
11 | template: {
12 | compilerOptions: {
13 | isCustomElement: tag => ['tt', 'center'].includes(tag)
14 | }
15 | }
16 | })
17 | ],
18 |
19 | define: {
20 | 'import.meta.env.PACKAGE_VERSION': JSON.stringify(packageJson.version)
21 | }
22 | })
23 |
--------------------------------------------------------------------------------